From 21c92afa7aa4ebfb7ef4d8c275df2e25bf4ec105 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Mon, 31 Aug 2026 19:57:38 +0800 Subject: [PATCH] =?UTF-8?q?task-23:=20=E6=8B=86=E5=88=86=20image-video=20/?= =?UTF-8?q?=20brand=20/=20permission=20/=20digital-human=20=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=20API=20=E8=87=B3=20types/modules/=EF=BC=8C=E6=97=A7?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E8=BD=AC=20re-export=20=E5=AD=98=E6=A0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend-vue/src/shared/api/brand.ts | 109 +------ frontend-vue/src/shared/api/digital-human.ts | 158 +--------- frontend-vue/src/shared/api/java-modules.ts | 272 +---------------- frontend-vue/src/shared/api/permission.ts | 86 +----- .../src/shared/api/types/modules/brand.ts | 108 +++++++ .../shared/api/types/modules/digital-human.ts | 157 ++++++++++ .../shared/api/types/modules/image-video.ts | 279 ++++++++++++++++++ .../shared/api/types/modules/permission.ts | 85 ++++++ .../tests/modules-image-video.test.ts | 217 ++++++++++++++ 9 files changed, 853 insertions(+), 618 deletions(-) create mode 100644 frontend-vue/src/shared/api/types/modules/brand.ts create mode 100644 frontend-vue/src/shared/api/types/modules/digital-human.ts create mode 100644 frontend-vue/src/shared/api/types/modules/image-video.ts create mode 100644 frontend-vue/src/shared/api/types/modules/permission.ts create mode 100644 frontend-vue/tests/modules-image-video.test.ts diff --git a/frontend-vue/src/shared/api/brand.ts b/frontend-vue/src/shared/api/brand.ts index 073a9773..4812cb9f 100644 --- a/frontend-vue/src/shared/api/brand.ts +++ b/frontend-vue/src/shared/api/brand.ts @@ -1,108 +1 @@ -import { requestDeleteJson, requestGetJson, requestPostJson } from '@/shared/api/http' - -function getCurrentUserId() { - const raw = typeof window === 'undefined' ? '' : window.localStorage.getItem('uid') || '' - const value = Number(raw) - if (!Number.isFinite(value) || value <= 0) { - throw new Error('未获取到用户ID') - } - return value -} - -export interface BrandExpandFolderItem { - absolutePath: string - relativePath: string -} - -export interface BrandExpandFolderResponse { - success: boolean - items?: BrandExpandFolderItem[] - error?: string -} - -export interface BrandTaskResultPaths { - zip_url?: string -} - -export interface BrandTaskItem { - id: number | string - status?: 'pending' | 'running' | 'success' | 'failed' | 'cancelled' | string - desc?: string - file_paths?: string[] - created_at?: string - progress_total?: number - progress_current?: number - result_paths?: BrandTaskResultPaths -} - -export interface BrandTaskListResponse { - success: boolean - items?: BrandTaskItem[] - error?: string -} - -export interface BrandTaskDetailResponse { - success: boolean - task?: BrandTaskItem - error?: string -} - -export interface BrandTaskMutationResponse { - success: boolean - task_id?: string - error?: string -} - -const API_PREFIX = '' - -export function getBrandTaskEventsUrl(taskId: string | number) { - return `${API_PREFIX}/api/brand/tasks/${taskId}/events?user_id=${encodeURIComponent(String(getCurrentUserId()))}` -} - -export function getBrandTaskDownloadUrl(taskId: string | number) { - return `${API_PREFIX}/api/brand/tasks/${taskId}/download?user_id=${encodeURIComponent(String(getCurrentUserId()))}` -} - -export function getBrandTemplateXlsxUrl() { - return `${API_PREFIX}/static/品牌文档格式_模板.xlsx` -} - -export function getBrandTemplateZipUrl() { - return `${API_PREFIX}/static/模板2-以文件夹方式上传.zip` -} - -export function expandBrandFolder(folder: string) { - return requestPostJson(`${API_PREFIX}/api/brand/expand-folder`, { folder }) -} - -export function expandBrandFolderRecursive(folder: string) { - return requestPostJson(`${API_PREFIX}/api/brand/expand-folder-recursive`, { folder }) -} - -export function runBrandNow(paths: string[], strategy: string) { - return requestPostJson(`${API_PREFIX}/api/brand/run`, { paths, strategy }) -} - -export function createBrandTask(paths: string[], strategy: string) { - return requestPostJson(`${API_PREFIX}/api/brand/tasks?userId=${encodeURIComponent(String(getCurrentUserId()))}`, { paths, strategy }) -} - -export function getBrandTasks() { - return requestGetJson(`${API_PREFIX}/api/brand/tasks?userId=${encodeURIComponent(String(getCurrentUserId()))}`) -} - -export function getBrandTask(taskId: string | number) { - return requestGetJson(`${API_PREFIX}/api/brand/tasks/${taskId}?userId=${encodeURIComponent(String(getCurrentUserId()))}`) -} - -export function cancelBrandTask(taskId: string | number) { - return requestPostJson(`${API_PREFIX}/api/brand/tasks/${taskId}/cancel?userId=${encodeURIComponent(String(getCurrentUserId()))}`) -} - -export function deleteBrandTask(taskId: string | number) { - return requestDeleteJson(`${API_PREFIX}/api/brand/tasks/${taskId}?userId=${encodeURIComponent(String(getCurrentUserId()))}`) -} - -export function createBrandTaskEvents(taskId: string | number) { - return new EventSource(getBrandTaskEventsUrl(taskId)) -} +export * from './types/modules/brand' diff --git a/frontend-vue/src/shared/api/digital-human.ts b/frontend-vue/src/shared/api/digital-human.ts index d8a13f15..684b0289 100644 --- a/frontend-vue/src/shared/api/digital-human.ts +++ b/frontend-vue/src/shared/api/digital-human.ts @@ -1,157 +1 @@ -import { get, post, del, type JavaApiResponse, unwrapJavaResponse } from '@/shared/api/http' - -const JAVA_API_PREFIX = '/newApi/api/digital-human/versions' - -/** - * 数字人版本信息 - */ -export interface DigitalHumanVersion { - id: number - version: string - ossObjectKey: string - fileSize: number - md5: string - changelog: string | null - minClientVersion: string | null - isLatest: boolean - status: 'DRAFT' | 'RELEASED' | 'DEPRECATED' - createdBy: string | null - createdAt: string - releasedAt: string | null - downloadUrl?: string -} - -/** - * 下载链接信息 - */ -export interface DownloadUrlVo { - version: string - downloadUrl: string - expiresIn: number -} - -/** - * 分页查询参数 - */ -export interface VersionListParams { - page?: number - pageSize?: number - status?: 'DRAFT' | 'RELEASED' | 'DEPRECATED' -} - -/** - * 分页结果 - */ -export interface PageResult { - records: T[] - total: number - size: number - current: number - pages: number -} - -/** - * 上传新版本 - */ -export function uploadDigitalHumanVersion( - file: File, - version: string, - changelog?: string, - minClientVersion?: string, -) { - const formData = new FormData() - formData.append('file', file) - formData.append('version', version) - if (changelog) formData.append('changelog', changelog) - if (minClientVersion) formData.append('minClientVersion', minClientVersion) - - return unwrapJavaResponse( - post>( - `${JAVA_API_PREFIX}/upload`, - formData, - { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }, - ), - ) -} - -/** - * 查询版本列表 - */ -export function getDigitalHumanVersions(params?: VersionListParams) { - return unwrapJavaResponse( - get>>( - JAVA_API_PREFIX, - { params }, - ), - ) -} - -/** - * 获取最新版本 - */ -export function getLatestDigitalHumanVersion() { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/latest`, - ), - ) -} - -/** - * 获取指定版本详情 - */ -export function getDigitalHumanVersion(version: string) { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/${version}`, - ), - ) -} - -/** - * 发布版本 - */ -export function releaseDigitalHumanVersion(version: string) { - return unwrapJavaResponse( - post>( - `${JAVA_API_PREFIX}/${version}/release`, - ), - ) -} - -/** - * 设为最新版本 - */ -export function setLatestDigitalHumanVersion(version: string) { - return unwrapJavaResponse( - post>( - `${JAVA_API_PREFIX}/${version}/set-latest`, - ), - ) -} - -/** - * 删除版本 - */ -export function deleteDigitalHumanVersion(version: string) { - return unwrapJavaResponse( - del>( - `${JAVA_API_PREFIX}/${version}`, - ), - ) -} - -/** - * 获取下载链接 - */ -export function getDigitalHumanVersionDownloadUrl(version: string) { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/${version}/download-url`, - ), - ) -} +export * from './types/modules/digital-human' diff --git a/frontend-vue/src/shared/api/java-modules.ts b/frontend-vue/src/shared/api/java-modules.ts index e6ce05f0..848bde50 100644 --- a/frontend-vue/src/shared/api/java-modules.ts +++ b/frontend-vue/src/shared/api/java-modules.ts @@ -22,6 +22,10 @@ export * from "./types/modules/shop-match.ts"; export * from "./types/modules/query-asin.ts"; export * from "./types/modules/withdraw.ts"; export * from "./types/modules/collect-data.ts"; +export * from "./types/modules/image-video.ts"; +export * from "./types/modules/brand.ts"; +export * from "./types/modules/permission.ts"; +export * from "./types/modules/digital-human.ts"; import type { ProductRiskCandidateVo, ProductRiskDashboardVo, @@ -166,274 +170,6 @@ export async function uploadTempFileToJava( } -// ========== 视频复刻 / 图生视频 ========== - -export interface ImageVideoDouyinCopyVo { - recognizedContent?: string; - scriptDraft?: string; - executeId?: string; - debugUrl?: string; -} - -export interface ImageVideoDouyinCopyPayload { - url: string; - api_key?: string; - t8_key?: string; - duration?: number; - proc_info?: { - type: string; - name: string; - proc_image: string[]; - properties: string; - }; -} - -export interface ImageVideoSecretStatusVo { - userId?: number; - configured?: boolean; - valid?: boolean; - expired?: boolean; - hasCopyApiKey?: boolean; - hasT8Key?: boolean; - hasT8VideoKey?: boolean; - hasVoiceApiKey?: boolean; - hasVoiceGroupId?: boolean; - copyApiKeyMasked?: string; - t8KeyMasked?: string; - t8VideoKeyMasked?: string; - voiceApiKeyMasked?: string; - voiceGroupIdMasked?: string; - expireDays?: number; - expiresAt?: string; -} - -export interface ImageVideoSecretSavePayload { - copyApiKey?: string; - t8Key?: string; - t8VideoKey?: string; - voiceApiKey?: string; - voiceGroupId?: string; - expireDays: number; -} - -export interface ImageVideoWorkflowParameters { - api_key_info: { - t8star_key: string; - t8_video_key: string; - ai_conductor_key: string; - }; - bg_info: { - type: number; - prompt: string; - bg_image: string; - }; - face_info: { - type: number; - model_figure: string; - model_image: string[]; - }; - proc_info: { - type: string; - name: string; - proc_image: string[]; - properties: string; - }; - text_info: { - type: number; - language: string; - text: string; - file_url: string; - }; - audio_info: { - audio_url: string; - type: number; - bgm_url: string; - mode: number; - voice_name: string; - }; - video_info: { - video_url: string; - share_url: string; - ref_video_mode: string; - mode: string; - draft: boolean; - model: string; - prompt: string; - ratio: string; - resolution: string; - duration: number; - }; -} - -export interface ImageVideoWorkflowResponse { - code?: number; - msg?: string; - data?: string; - execute_id?: string; - debug_url?: string; - [key: string]: unknown; -} - -export type ImageVideoAsyncTaskStatus = 'PENDING' | 'RUNNING' | 'WAITING' | 'POLLING' | 'SUCCESS' | 'FAILED' - -export interface ImageVideoAsyncTaskVo { - taskId: number; - taskType: string; - status: ImageVideoAsyncTaskStatus; - cozeExecuteId?: string; - cozeStatus?: string; - debugUrl?: string; - result?: unknown; - errorMessage?: string; - submittedAt?: string; - completedAt?: string; -} - -export interface ImageVideoMediaUploadVo { - url: string; - objectKey: string; - originalFilename: string; - mediaType: "image" | "video" | "audio" | string; -} - -export interface ImageVideoVoiceWorkflowResponse { - code?: number; - msg?: string; - data?: unknown; - execute_id?: string; - debug_url?: string; - [key: string]: unknown; -} - -export interface ImageVideoWorkflowRunRequest { - userId: number; - parameters: ImageVideoWorkflowParameters; -} - -export interface ImageVideoWorkflowResultRequest { - userId: number; - executeId: string; -} - -export function getImageVideoSecretStatus() { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/image-video/secrets`, - { params: { user_id: getCurrentUserId() } }, - ), - ); -} - -export function saveImageVideoSecrets(payload: ImageVideoSecretSavePayload) { - return unwrapJavaResponse( - put, ImageVideoSecretSavePayload & { userId: number }>( - `${JAVA_API_PREFIX}/image-video/secrets`, - { ...payload, userId: getCurrentUserId() }, - ), - ); -} - -export function runImageVideoDouyinCopy(payload: ImageVideoDouyinCopyPayload) { - return unwrapJavaResponse( - post, ImageVideoDouyinCopyPayload & { userId: number }>( - `${JAVA_API_PREFIX}/image-video/douyin-copy`, - { userId: getCurrentUserId(), ...payload }, - ), - ); -} - -export function runImageVideoWorkflow(parameters: ImageVideoWorkflowParameters) { - return unwrapJavaResponse( - post, ImageVideoWorkflowRunRequest>( - `${JAVA_API_PREFIX}/image-video/workflow/run`, - { userId: getCurrentUserId(), parameters }, - ), - ); -} - -export function getImageVideoWorkflowResult(executeId: string) { - return unwrapJavaResponse( - post, ImageVideoWorkflowResultRequest>( - `${JAVA_API_PREFIX}/image-video/workflow/result`, - { userId: getCurrentUserId(), executeId }, - ), - ); -} - -export function getImageVideoAsyncTask(taskId: number) { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/image-video/tasks/${taskId}`, - { params: { user_id: getCurrentUserId() } }, - ), - ); -} - -export async function uploadImageVideoMedia(file: File) { - const response = await uploadTempFileToJava(file, { - uploadToOss: true, - moduleType: "IMAGE_VIDEO", - }); - if (!response.success) { - throw new Error(response.message || "请求失败"); - } - const data = response.data; - if (!data?.url || !data.objectKey) { - throw new Error("OSS 上传未返回有效 URL"); - } - return { - url: data.url, - objectKey: data.objectKey, - originalFilename: data.originalFilename || file.name, - mediaType: data.mediaType || resolveImageVideoMediaType(file), - } as ImageVideoMediaUploadVo; -} - -function resolveImageVideoMediaType(file: File) { - const type = (file.type || "").toLowerCase(); - const name = (file.name || "").toLowerCase(); - if (type.startsWith("image/") || /\.(png|jpe?g|webp|gif|bmp|svg)$/.test(name)) return "image"; - if (type.startsWith("video/") || /\.(mp4|mov|webm|m4v|ogg|avi|mkv)$/.test(name)) return "video"; - if (type.startsWith("audio/") || /\.(mp3|wav|m4a|aac|flac)$/.test(name)) return "audio"; - return "file"; -} - -export function listImageVideoVoices(name = "") { - return unwrapJavaResponse( - post, { userId: number; name: string }>( - `${JAVA_API_PREFIX}/image-video/voice/list`, - { userId: getCurrentUserId(), name }, - ), - ); -} - -export function deleteImageVideoVoice(voiceId: string) { - return unwrapJavaResponse( - post, { userId: number; voiceId: string }>( - `${JAVA_API_PREFIX}/image-video/voice/delete`, - { userId: getCurrentUserId(), voiceId }, - ), - ); -} - -export function cloneImageVideoVoice(payload: { name?: string; audioUrl?: string; videoUrl?: string }) { - return unwrapJavaResponse( - post, { userId: number; name?: string; audioUrl?: string; videoUrl?: string }>( - `${JAVA_API_PREFIX}/image-video/voice/clone`, - { userId: getCurrentUserId(), ...payload }, - ), - ); -} - -export function synthesizeImageVideoVoice(payload: { text: string; voiceId: string }) { - return unwrapJavaResponse( - post, { userId: number; text: string; voiceId: string }>( - `${JAVA_API_PREFIX}/image-video/voice/synthesis`, - { userId: getCurrentUserId(), ...payload }, - ), - ); -} - export function getJavaDownloadUrl(path: string) { let raw = path.startsWith("http://") || path.startsWith("https://") diff --git a/frontend-vue/src/shared/api/permission.ts b/frontend-vue/src/shared/api/permission.ts index 7734652f..882a74a7 100644 --- a/frontend-vue/src/shared/api/permission.ts +++ b/frontend-vue/src/shared/api/permission.ts @@ -1,85 +1 @@ -import { requestGetJson } from '@/shared/api/http' - -export interface PermissionMenuItem { - id: number | string - name?: string - column_key?: string - columnKey?: string - route_path?: string - routePath?: string - menu_type?: string - parent_id?: number | string | null - parentId?: number | string | null - root_column_key?: string - rootColumnKey?: string - sort_order?: number - created_at?: string -} - -interface PermissionMenuResponse { - success: boolean - data?: PermissionMenuItem[] - items?: PermissionMenuItem[] - error?: string - message?: string -} - -function getCurrentUserId() { - const raw = typeof window === 'undefined' ? '' : window.localStorage.getItem('uid') || '' - const value = Number(raw) - if (!Number.isFinite(value) || value <= 0) { - throw new Error('未获取到用户ID') - } - return value -} - -function getAppPermissionCacheKey(uid: number) { - return `app_column_permissions:${String(uid)}` -} - -function getAuthToken() { - return typeof window === 'undefined' ? '' : window.localStorage.getItem('aiimage_auth_token') || '' -} - -function normalizeColumnKeys(items: PermissionMenuItem[] | undefined) { - const keys = new Set() - for (const item of items || []) { - for (const value of [item.column_key, item.columnKey, item.route_path, item.routePath]) { - const key = String(value || '').trim().toLowerCase() - if (key) { - keys.add(key) - } - } - } - return Array.from(keys) -} - -export async function getCurrentUserAppColumnKeys() { - const uid = getCurrentUserId() - const cacheKey = getAppPermissionCacheKey(uid) - - const headers: Record = {} - const token = getAuthToken() - if (token) { - headers.Authorization = `Bearer ${token}` - } - - const res = await requestGetJson( - `/newApi/api/admin/permission-users/${encodeURIComponent(String(uid))}/column-permissions`, - { - params: { menuType: 'app' }, - headers, - }, - ) - - if (!res.success) { - throw new Error(res.error || res.message || '获取菜单权限失败') - } - - const items = res.data || res.items || [] - try { - window.localStorage.setItem(cacheKey, JSON.stringify(items)) - } catch (_error) {} - - return normalizeColumnKeys(items) -} +export * from './types/modules/permission' diff --git a/frontend-vue/src/shared/api/types/modules/brand.ts b/frontend-vue/src/shared/api/types/modules/brand.ts new file mode 100644 index 00000000..8f907e04 --- /dev/null +++ b/frontend-vue/src/shared/api/types/modules/brand.ts @@ -0,0 +1,108 @@ +import { requestDeleteJson, requestGetJson, requestPostJson } from '../../http.ts' + +function getCurrentUserId() { + const raw = typeof window === 'undefined' ? '' : window.localStorage.getItem('uid') || '' + const value = Number(raw) + if (!Number.isFinite(value) || value <= 0) { + throw new Error('未获取到用户ID') + } + return value +} + +export interface BrandExpandFolderItem { + absolutePath: string + relativePath: string +} + +export interface BrandExpandFolderResponse { + success: boolean + items?: BrandExpandFolderItem[] + error?: string +} + +export interface BrandTaskResultPaths { + zip_url?: string +} + +export interface BrandTaskItem { + id: number | string + status?: 'pending' | 'running' | 'success' | 'failed' | 'cancelled' | string + desc?: string + file_paths?: string[] + created_at?: string + progress_total?: number + progress_current?: number + result_paths?: BrandTaskResultPaths +} + +export interface BrandTaskListResponse { + success: boolean + items?: BrandTaskItem[] + error?: string +} + +export interface BrandTaskDetailResponse { + success: boolean + task?: BrandTaskItem + error?: string +} + +export interface BrandTaskMutationResponse { + success: boolean + task_id?: string + error?: string +} + +const API_PREFIX = '' + +export function getBrandTaskEventsUrl(taskId: string | number) { + return `${API_PREFIX}/api/brand/tasks/${taskId}/events?user_id=${encodeURIComponent(String(getCurrentUserId()))}` +} + +export function getBrandTaskDownloadUrl(taskId: string | number) { + return `${API_PREFIX}/api/brand/tasks/${taskId}/download?user_id=${encodeURIComponent(String(getCurrentUserId()))}` +} + +export function getBrandTemplateXlsxUrl() { + return `${API_PREFIX}/static/品牌文档格式_模板.xlsx` +} + +export function getBrandTemplateZipUrl() { + return `${API_PREFIX}/static/模板2-以文件夹方式上传.zip` +} + +export function expandBrandFolder(folder: string) { + return requestPostJson(`${API_PREFIX}/api/brand/expand-folder`, { folder }) +} + +export function expandBrandFolderRecursive(folder: string) { + return requestPostJson(`${API_PREFIX}/api/brand/expand-folder-recursive`, { folder }) +} + +export function runBrandNow(paths: string[], strategy: string) { + return requestPostJson(`${API_PREFIX}/api/brand/run`, { paths, strategy }) +} + +export function createBrandTask(paths: string[], strategy: string) { + return requestPostJson(`${API_PREFIX}/api/brand/tasks?userId=${encodeURIComponent(String(getCurrentUserId()))}`, { paths, strategy }) +} + +export function getBrandTasks() { + return requestGetJson(`${API_PREFIX}/api/brand/tasks?userId=${encodeURIComponent(String(getCurrentUserId()))}`) +} + +export function getBrandTask(taskId: string | number) { + return requestGetJson(`${API_PREFIX}/api/brand/tasks/${taskId}?userId=${encodeURIComponent(String(getCurrentUserId()))}`) +} + +export function cancelBrandTask(taskId: string | number) { + return requestPostJson(`${API_PREFIX}/api/brand/tasks/${taskId}/cancel?userId=${encodeURIComponent(String(getCurrentUserId()))}`) +} + +export function deleteBrandTask(taskId: string | number) { + return requestDeleteJson(`${API_PREFIX}/api/brand/tasks/${taskId}?userId=${encodeURIComponent(String(getCurrentUserId()))}`) +} + +export function createBrandTaskEvents(taskId: string | number) { + return new EventSource(getBrandTaskEventsUrl(taskId)) +} diff --git a/frontend-vue/src/shared/api/types/modules/digital-human.ts b/frontend-vue/src/shared/api/types/modules/digital-human.ts new file mode 100644 index 00000000..6e1d5a2c --- /dev/null +++ b/frontend-vue/src/shared/api/types/modules/digital-human.ts @@ -0,0 +1,157 @@ +import { get, post, del, type JavaApiResponse, unwrapJavaResponse } from '../../http.ts' + +const JAVA_API_PREFIX = '/newApi/api/digital-human/versions' + +/** + * 数字人版本信息 + */ +export interface DigitalHumanVersion { + id: number + version: string + ossObjectKey: string + fileSize: number + md5: string + changelog: string | null + minClientVersion: string | null + isLatest: boolean + status: 'DRAFT' | 'RELEASED' | 'DEPRECATED' + createdBy: string | null + createdAt: string + releasedAt: string | null + downloadUrl?: string +} + +/** + * 下载链接信息 + */ +export interface DownloadUrlVo { + version: string + downloadUrl: string + expiresIn: number +} + +/** + * 分页查询参数 + */ +export interface VersionListParams { + page?: number + pageSize?: number + status?: 'DRAFT' | 'RELEASED' | 'DEPRECATED' +} + +/** + * 分页结果 + */ +export interface PageResult { + records: T[] + total: number + size: number + current: number + pages: number +} + +/** + * 上传新版本 + */ +export function uploadDigitalHumanVersion( + file: File, + version: string, + changelog?: string, + minClientVersion?: string, +) { + const formData = new FormData() + formData.append('file', file) + formData.append('version', version) + if (changelog) formData.append('changelog', changelog) + if (minClientVersion) formData.append('minClientVersion', minClientVersion) + + return unwrapJavaResponse( + post>( + `${JAVA_API_PREFIX}/upload`, + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }, + ), + ) +} + +/** + * 查询版本列表 + */ +export function getDigitalHumanVersions(params?: VersionListParams) { + return unwrapJavaResponse( + get>>( + JAVA_API_PREFIX, + { params }, + ), + ) +} + +/** + * 获取最新版本 + */ +export function getLatestDigitalHumanVersion() { + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/latest`, + ), + ) +} + +/** + * 获取指定版本详情 + */ +export function getDigitalHumanVersion(version: string) { + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/${version}`, + ), + ) +} + +/** + * 发布版本 + */ +export function releaseDigitalHumanVersion(version: string) { + return unwrapJavaResponse( + post>( + `${JAVA_API_PREFIX}/${version}/release`, + ), + ) +} + +/** + * 设为最新版本 + */ +export function setLatestDigitalHumanVersion(version: string) { + return unwrapJavaResponse( + post>( + `${JAVA_API_PREFIX}/${version}/set-latest`, + ), + ) +} + +/** + * 删除版本 + */ +export function deleteDigitalHumanVersion(version: string) { + return unwrapJavaResponse( + del>( + `${JAVA_API_PREFIX}/${version}`, + ), + ) +} + +/** + * 获取下载链接 + */ +export function getDigitalHumanVersionDownloadUrl(version: string) { + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/${version}/download-url`, + ), + ) +} diff --git a/frontend-vue/src/shared/api/types/modules/image-video.ts b/frontend-vue/src/shared/api/types/modules/image-video.ts new file mode 100644 index 00000000..7fb756b6 --- /dev/null +++ b/frontend-vue/src/shared/api/types/modules/image-video.ts @@ -0,0 +1,279 @@ +import { get, post, put, type JavaApiResponse, unwrapJavaResponse } from '../../http.ts' +import { buildJavaUrl, JAVA_API_PREFIX } from '../../url.ts' +import { getCurrentUserId } from '../../user.ts' + +export interface ImageVideoDouyinCopyVo { + recognizedContent?: string; + scriptDraft?: string; + executeId?: string; + debugUrl?: string; +} + +export interface ImageVideoDouyinCopyPayload { + url: string; + api_key?: string; + t8_key?: string; + duration?: number; + proc_info?: { + type: string; + name: string; + proc_image: string[]; + properties: string; + }; +} + +export interface ImageVideoSecretStatusVo { + userId?: number; + configured?: boolean; + valid?: boolean; + expired?: boolean; + hasCopyApiKey?: boolean; + hasT8Key?: boolean; + hasT8VideoKey?: boolean; + hasVoiceApiKey?: boolean; + hasVoiceGroupId?: boolean; + copyApiKeyMasked?: string; + t8KeyMasked?: string; + t8VideoKeyMasked?: string; + voiceApiKeyMasked?: string; + voiceGroupIdMasked?: string; + expireDays?: number; + expiresAt?: string; +} + +export interface ImageVideoSecretSavePayload { + copyApiKey?: string; + t8Key?: string; + t8VideoKey?: string; + voiceApiKey?: string; + voiceGroupId?: string; + expireDays: number; +} + +export interface ImageVideoWorkflowParameters { + api_key_info: { + t8star_key: string; + t8_video_key: string; + ai_conductor_key: string; + }; + bg_info: { + type: number; + prompt: string; + bg_image: string; + }; + face_info: { + type: number; + model_figure: string; + model_image: string[]; + }; + proc_info: { + type: string; + name: string; + proc_image: string[]; + properties: string; + }; + text_info: { + type: number; + language: string; + text: string; + file_url: string; + }; + audio_info: { + audio_url: string; + type: number; + bgm_url: string; + mode: number; + voice_name: string; + }; + video_info: { + video_url: string; + share_url: string; + ref_video_mode: string; + mode: string; + draft: boolean; + model: string; + prompt: string; + ratio: string; + resolution: string; + duration: number; + }; +} + +export interface ImageVideoWorkflowResponse { + code?: number; + msg?: string; + data?: string; + execute_id?: string; + debug_url?: string; + [key: string]: unknown; +} + +export type ImageVideoAsyncTaskStatus = 'PENDING' | 'RUNNING' | 'WAITING' | 'POLLING' | 'SUCCESS' | 'FAILED' + +export interface ImageVideoAsyncTaskVo { + taskId: number; + taskType: string; + status: ImageVideoAsyncTaskStatus; + cozeExecuteId?: string; + cozeStatus?: string; + debugUrl?: string; + result?: unknown; + errorMessage?: string; + submittedAt?: string; + completedAt?: string; +} + +export interface ImageVideoMediaUploadVo { + url: string; + objectKey: string; + originalFilename: string; + mediaType: "image" | "video" | "audio" | string; +} + +export interface ImageVideoVoiceWorkflowResponse { + code?: number; + msg?: string; + data?: unknown; + execute_id?: string; + debug_url?: string; + [key: string]: unknown; +} + +export interface ImageVideoWorkflowRunRequest { + userId: number; + parameters: ImageVideoWorkflowParameters; +} + +export interface ImageVideoWorkflowResultRequest { + userId: number; + executeId: string; +} + +export function getImageVideoSecretStatus() { + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/image-video/secrets`, + { params: { user_id: getCurrentUserId() } }, + ), + ); +} + +export function saveImageVideoSecrets(payload: ImageVideoSecretSavePayload) { + return unwrapJavaResponse( + put, ImageVideoSecretSavePayload & { userId: number }>( + `${JAVA_API_PREFIX}/image-video/secrets`, + { ...payload, userId: getCurrentUserId() }, + ), + ); +} + +export function runImageVideoDouyinCopy(payload: ImageVideoDouyinCopyPayload) { + return unwrapJavaResponse( + post, ImageVideoDouyinCopyPayload & { userId: number }>( + `${JAVA_API_PREFIX}/image-video/douyin-copy`, + { userId: getCurrentUserId(), ...payload }, + ), + ); +} + +export function runImageVideoWorkflow(parameters: ImageVideoWorkflowParameters) { + return unwrapJavaResponse( + post, ImageVideoWorkflowRunRequest>( + `${JAVA_API_PREFIX}/image-video/workflow/run`, + { userId: getCurrentUserId(), parameters }, + ), + ); +} + +export function getImageVideoWorkflowResult(executeId: string) { + return unwrapJavaResponse( + post, ImageVideoWorkflowResultRequest>( + `${JAVA_API_PREFIX}/image-video/workflow/result`, + { userId: getCurrentUserId(), executeId }, + ), + ); +} + +export function getImageVideoAsyncTask(taskId: number) { + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/image-video/tasks/${taskId}`, + { params: { user_id: getCurrentUserId() } }, + ), + ); +} + +function resolveImageVideoMediaType(file: File) { + const type = (file.type || "").toLowerCase(); + const name = (file.name || "").toLowerCase(); + if (type.startsWith("image/") || /\.(png|jpe?g|webp|gif|bmp|svg)$/.test(name)) return "image"; + if (type.startsWith("video/") || /\.(mp4|mov|webm|m4v|ogg|avi|mkv)$/.test(name)) return "video"; + if (type.startsWith("audio/") || /\.(mp3|wav|m4a|aac|flac)$/.test(name)) return "audio"; + return "file"; +} + +export async function uploadImageVideoMedia(file: File) { + const formData = new FormData(); + formData.append("file", file); + formData.append("uploadToOss", "true"); + formData.append("moduleType", "IMAGE_VIDEO"); + const response = await post>( + `${JAVA_API_PREFIX}/files/upload`, + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + timeout: 120000, + }, + ); + if (!response.success) { + throw new Error(response.message || "请求失败"); + } + const data = response.data; + if (!data?.url || !data.objectKey) { + throw new Error("OSS 上传未返回有效 URL"); + } + return { + url: data.url, + objectKey: data.objectKey, + originalFilename: data.originalFilename || file.name, + mediaType: data.mediaType || resolveImageVideoMediaType(file), + } as ImageVideoMediaUploadVo; +} + +export function listImageVideoVoices(name = "") { + return unwrapJavaResponse( + post, { userId: number; name: string }>( + `${JAVA_API_PREFIX}/image-video/voice/list`, + { userId: getCurrentUserId(), name }, + ), + ); +} + +export function deleteImageVideoVoice(voiceId: string) { + return unwrapJavaResponse( + post, { userId: number; voiceId: string }>( + `${JAVA_API_PREFIX}/image-video/voice/delete`, + { userId: getCurrentUserId(), voiceId }, + ), + ); +} + +export function cloneImageVideoVoice(payload: { name?: string; audioUrl?: string; videoUrl?: string }) { + return unwrapJavaResponse( + post, { userId: number; name?: string; audioUrl?: string; videoUrl?: string }>( + `${JAVA_API_PREFIX}/image-video/voice/clone`, + { userId: getCurrentUserId(), ...payload }, + ), + ); +} + +export function synthesizeImageVideoVoice(payload: { text: string; voiceId: string }) { + return unwrapJavaResponse( + post, { userId: number; text: string; voiceId: string }>( + `${JAVA_API_PREFIX}/image-video/voice/synthesis`, + { userId: getCurrentUserId(), ...payload }, + ), + ); +} diff --git a/frontend-vue/src/shared/api/types/modules/permission.ts b/frontend-vue/src/shared/api/types/modules/permission.ts new file mode 100644 index 00000000..c466166e --- /dev/null +++ b/frontend-vue/src/shared/api/types/modules/permission.ts @@ -0,0 +1,85 @@ +import { requestGetJson } from '../../http.ts' + +export interface PermissionMenuItem { + id: number | string + name?: string + column_key?: string + columnKey?: string + route_path?: string + routePath?: string + menu_type?: string + parent_id?: number | string | null + parentId?: number | string | null + root_column_key?: string + rootColumnKey?: string + sort_order?: number + created_at?: string +} + +interface PermissionMenuResponse { + success: boolean + data?: PermissionMenuItem[] + items?: PermissionMenuItem[] + error?: string + message?: string +} + +function getCurrentUserId() { + const raw = typeof window === 'undefined' ? '' : window.localStorage.getItem('uid') || '' + const value = Number(raw) + if (!Number.isFinite(value) || value <= 0) { + throw new Error('未获取到用户ID') + } + return value +} + +function getAppPermissionCacheKey(uid: number) { + return `app_column_permissions:${String(uid)}` +} + +function getAuthToken() { + return typeof window === 'undefined' ? '' : window.localStorage.getItem('aiimage_auth_token') || '' +} + +function normalizeColumnKeys(items: PermissionMenuItem[] | undefined) { + const keys = new Set() + for (const item of items || []) { + for (const value of [item.column_key, item.columnKey, item.route_path, item.routePath]) { + const key = String(value || '').trim().toLowerCase() + if (key) { + keys.add(key) + } + } + } + return Array.from(keys) +} + +export async function getCurrentUserAppColumnKeys() { + const uid = getCurrentUserId() + const cacheKey = getAppPermissionCacheKey(uid) + + const headers: Record = {} + const token = getAuthToken() + if (token) { + headers.Authorization = `Bearer ${token}` + } + + const res = await requestGetJson( + `/newApi/api/admin/permission-users/${encodeURIComponent(String(uid))}/column-permissions`, + { + params: { menuType: 'app' }, + headers, + }, + ) + + if (!res.success) { + throw new Error(res.error || res.message || '获取菜单权限失败') + } + + const items = res.data || res.items || [] + try { + window.localStorage.setItem(cacheKey, JSON.stringify(items)) + } catch (_error) {} + + return normalizeColumnKeys(items) +} diff --git a/frontend-vue/tests/modules-image-video.test.ts b/frontend-vue/tests/modules-image-video.test.ts new file mode 100644 index 00000000..78d903c4 --- /dev/null +++ b/frontend-vue/tests/modules-image-video.test.ts @@ -0,0 +1,217 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { http } from '../src/shared/api/http.ts' +import { + getImageVideoSecretStatus, + saveImageVideoSecrets, + runImageVideoDouyinCopy, + runImageVideoWorkflow, + getImageVideoWorkflowResult, + getImageVideoAsyncTask, + uploadImageVideoMedia, + listImageVideoVoices, + deleteImageVideoVoice, + cloneImageVideoVoice, + synthesizeImageVideoVoice, +} from '../src/shared/api/types/modules/image-video.ts' +import { + expandBrandFolderRecursive, + getBrandTasks, + createBrandTask, + getBrandTaskEventsUrl, +} from '../src/shared/api/types/modules/brand.ts' +import { + getCurrentUserAppColumnKeys, + type PermissionMenuItem, +} from '../src/shared/api/types/modules/permission.ts' +import { + getDigitalHumanVersions, + releaseDigitalHumanVersion, + getDigitalHumanVersionDownloadUrl, +} from '../src/shared/api/types/modules/digital-human.ts' + +function setupWindow() { + const store = new Map([['uid', '42']]) + ;(globalThis as Record).window = { + localStorage: { + getItem: (key: string) => store.get(key) ?? null, + setItem: (key: string, value: string) => void store.set(key, value), + }, + location: { origin: 'http://localhost' }, + } +} + +function mockRequest( + t: Parameters[1] extends (t: infer T) => unknown ? T : never, + impl: (config: { url?: string; method?: string; params?: Record; data?: unknown; timeout?: number }) => Promise, +) { + t.mock.method(http, 'request', impl as never) +} + +const okResponse = (data: unknown) => Promise.resolve({ data: { success: true, message: 'ok', data } }) + +test('test_image_video_secrets', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; params?: Record; data?: unknown }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ userId: 42, configured: true }) + }) + await getImageVideoSecretStatus() + await saveImageVideoSecrets({ expireDays: 30 }) + assert.equal(calls[0].url, '/newApi/api/image-video/secrets') + assert.deepEqual(calls[0].params, { user_id: 42 }) + assert.equal(calls[1].url, '/newApi/api/image-video/secrets') + assert.equal(calls[1].method, 'PUT') + assert.deepEqual(calls[1].data, { expireDays: 30, userId: 42 }) +}) + +test('test_image_video_workflow', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; data?: unknown }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ taskId: 1, taskType: 'workflow', status: 'RUNNING' }) + }) + const params = { api_key_info: { t8star_key: 'a', t8_video_key: 'b', ai_conductor_key: 'c' } } + await runImageVideoWorkflow(params as never) + await getImageVideoWorkflowResult('exec-1') + assert.equal(calls[0].url, '/newApi/api/image-video/workflow/run') + assert.equal(calls[0].method, 'POST') + assert.deepEqual(calls[0].data, { userId: 42, parameters: params }) + assert.equal(calls[1].url, '/newApi/api/image-video/workflow/result') + assert.deepEqual(calls[1].data, { userId: 42, executeId: 'exec-1' }) +}) + +test('test_image_video_douyin_async', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; data?: unknown; params?: Record }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ taskId: 2, taskType: 'copy', status: 'PENDING' }) + }) + await runImageVideoDouyinCopy({ url: 'https://x.com/v' }) + await getImageVideoAsyncTask(7) + assert.equal(calls[0].url, '/newApi/api/image-video/douyin-copy') + assert.equal(calls[0].method, 'POST') + assert.deepEqual(calls[0].data, { userId: 42, url: 'https://x.com/v' }) + assert.equal(calls[1].url, '/newApi/api/image-video/tasks/7') + assert.deepEqual(calls[1].params, { user_id: 42 }) +}) + +test('test_image_video_voice', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; data?: unknown }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ taskId: 3, taskType: 'voice', status: 'SUCCESS' }) + }) + await listImageVideoVoices('v1') + await deleteImageVideoVoice('vid-1') + await cloneImageVideoVoice({ name: 'clone' }) + await synthesizeImageVideoVoice({ text: '你好', voiceId: 'vid-1' }) + assert.equal(calls[0].url, '/newApi/api/image-video/voice/list') + assert.deepEqual(calls[0].data, { userId: 42, name: 'v1' }) + assert.equal(calls[1].url, '/newApi/api/image-video/voice/delete') + assert.deepEqual(calls[1].data, { userId: 42, voiceId: 'vid-1' }) + assert.equal(calls[2].url, '/newApi/api/image-video/voice/clone') + assert.deepEqual(calls[2].data, { userId: 42, name: 'clone' }) + assert.equal(calls[3].url, '/newApi/api/image-video/voice/synthesis') + assert.deepEqual(calls[3].data, { userId: 42, text: '你好', voiceId: 'vid-1' }) +}) + +test('test_image_video_upload_media', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; data?: unknown }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ url: 'https://oss/x', objectKey: 'k1', originalFilename: 'a.png', mediaType: 'image' }) + }) + const file = new File(['x'], 'a.png', { type: 'image/png' }) + const result = await uploadImageVideoMedia(file) + assert.equal(calls[0].url, '/newApi/api/files/upload') + assert.equal(calls[0].method, 'POST') + assert.ok(calls[0].data instanceof FormData) + assert.deepEqual(result, { url: 'https://oss/x', objectKey: 'k1', originalFilename: 'a.png', mediaType: 'image' }) +}) + +test('test_brand_tasks', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; data?: unknown }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return Promise.resolve({ data: { success: true, items: [{ id: 1 }] } }) + }) + await getBrandTasks() + await createBrandTask(['/tmp/a'], 'STRATEGY_A') + await expandBrandFolderRecursive('/tmp/a') + assert.equal(calls[0].url, '/api/brand/tasks?userId=42') + assert.equal(calls[0].method, 'GET') + assert.equal(calls[1].url, '/api/brand/tasks?userId=42') + assert.equal(calls[1].method, 'POST') + assert.deepEqual(calls[1].data, { paths: ['/tmp/a'], strategy: 'STRATEGY_A' }) + assert.equal(calls[2].url, '/api/brand/expand-folder-recursive') + assert.equal(calls[2].method, 'POST') + assert.deepEqual(calls[2].data, { folder: '/tmp/a' }) + assert.equal( + getBrandTaskEventsUrl(7), + '/api/brand/tasks/7/events?user_id=42', + ) +}) + +test('test_permission_column_keys', async (t) => { + setupWindow() + mockRequest(t, () => + Promise.resolve({ + data: { success: true, data: [{ column_key: 'collect-data' }, { columnKey: 'Shop-Match' }] }, + }), + ) + const keys = await getCurrentUserAppColumnKeys() + assert.deepEqual(keys, ['collect-data', 'shop-match']) +}) + +test('test_digital_human_versions', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; params?: Record }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ records: [{ id: 1, version: '1.0.0' }], total: 1 }) + }) + await getDigitalHumanVersions({ page: 1, pageSize: 10 }) + await releaseDigitalHumanVersion('1.0.0') + await getDigitalHumanVersionDownloadUrl('1.0.0') + assert.equal(calls[0].url, '/newApi/api/digital-human/versions') + assert.deepEqual(calls[0].params, { page: 1, pageSize: 10 }) + assert.equal(calls[1].url, '/newApi/api/digital-human/versions/1.0.0/release') + assert.equal(calls[1].method, 'POST') + assert.equal(calls[2].url, '/newApi/api/digital-human/versions/1.0.0/download-url') +}) + +test('test_image_video_export_compat', async (t) => { + setupWindow() + const fromJavaModules = await import('../src/shared/api/java-modules.ts') + const fromImageVideo = await import('../src/shared/api/types/modules/image-video.ts') + const imageVideoNames = [ + 'getImageVideoSecretStatus', 'saveImageVideoSecrets', 'runImageVideoDouyinCopy', + 'runImageVideoWorkflow', 'getImageVideoWorkflowResult', 'getImageVideoAsyncTask', + 'uploadImageVideoMedia', 'listImageVideoVoices', 'deleteImageVideoVoice', + 'cloneImageVideoVoice', 'synthesizeImageVideoVoice', + ] + for (const name of imageVideoNames) { + assert.equal(fromJavaModules[name], fromImageVideo[name], `${name} 应为同一引用`) + } + const fromBrand = await import('../src/shared/api/types/modules/brand.ts') + assert.equal(typeof fromBrand.expandBrandFolderRecursive, 'function') + const item: PermissionMenuItem = { id: 1, column_key: 'x' } + assert.equal(item.column_key, 'x') +}) + +test('test_image_video_unwrap_and_error', async (t) => { + setupWindow() + mockRequest(t, () => okResponse({ taskId: 1, taskType: 't', status: 'RUNNING' })) + const result = await getImageVideoAsyncTask(1) + assert.equal(result.taskId, 1) + + mockRequest(t, () => Promise.resolve({ data: { success: false, message: '密钥未配置' } })) + await assert.rejects(getImageVideoSecretStatus(), /密钥未配置/) +})