From 77ccaf8acdfa1421b6e94c189e1ae7e0691679c0 Mon Sep 17 00:00:00 2001 From: imeepos Date: Mon, 1 Sep 2025 14:47:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8D=87=E7=BA=A7=E5=9B=BE=E5=83=8F?= =?UTF-8?q?=E7=94=9F=E6=88=90SDK=E6=94=AF=E6=8C=81multipart/form-data?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 升级generateImage方法支持multipart/form-data提交格式 - 添加aspect_ratio参数,默认9:16比例 - 更新默认模型为gemini-2.5-flash-image-preview - 为getTaskStatus方法添加失败重试机制,最多重试3次,每次间隔5秒 - 新增useSdk hook封装SDK使用 - 更新示例页面集成图像生成功能 --- src/hooks/index.ts | 4 + src/hooks/useAd.ts | 2 +- src/hooks/useSdk.ts | 6 + src/pages/index/index.tsx | 19 +- src/sdk/index.ts | 362 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 385 insertions(+), 8 deletions(-) create mode 100644 src/hooks/index.ts create mode 100644 src/hooks/useSdk.ts create mode 100644 src/sdk/index.ts diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..bde49ec --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,4 @@ + + +export { useAd } from './useAd' +export { useSdk } from './useSdk' \ No newline at end of file diff --git a/src/hooks/useAd.ts b/src/hooks/useAd.ts index ed95730..dcf08de 100644 --- a/src/hooks/useAd.ts +++ b/src/hooks/useAd.ts @@ -8,7 +8,7 @@ interface UseAdReturn { } export function useAd(): UseAdReturn { - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(true); const adRef = useRef(null); useEffect(() => { diff --git a/src/hooks/useSdk.ts b/src/hooks/useSdk.ts new file mode 100644 index 0000000..492c377 --- /dev/null +++ b/src/hooks/useSdk.ts @@ -0,0 +1,6 @@ +import { bowongAI } from "../sdk"; + + +export function useSdk() { + return bowongAI; +} \ No newline at end of file diff --git a/src/pages/index/index.tsx b/src/pages/index/index.tsx index ba66358..44fcd03 100644 --- a/src/pages/index/index.tsx +++ b/src/pages/index/index.tsx @@ -1,23 +1,28 @@ import { View, Text, Button } from '@tarojs/components' import { useLoad } from '@tarojs/taro' import './index.css' -import { useAd } from '../../hooks/useAd' +import { useAd, useSdk } from '../../hooks/index' export default function Index() { - const { showAd, loadAd } = useAd() + const sdk = useSdk() useLoad(() => { - loadAd() - return () => {} + return () => { } }) return ( Hello world Ymm !!!! + sdk.chooseAndGenerateImage().then(task_id => { + console.log({ task_id }) + return sdk.getTaskStatus(task_id) + }).then(res => { + console.log(res) + }) + }}> + 生图 + ) } diff --git a/src/sdk/index.ts b/src/sdk/index.ts new file mode 100644 index 0000000..7be246a --- /dev/null +++ b/src/sdk/index.ts @@ -0,0 +1,362 @@ +import Taro from "@tarojs/taro" + +/** + * API 响应基础接口 + */ +interface ApiResponse { + status: boolean | string; + data: T; + msg: string; +} + +/** + * 文件上传参数 + */ +interface UploadParams { + /** 文件路径(小程序中的临时文件路径) */ + filePath: string; + /** 文件名,可选,默认会自动生成 */ + name?: string; + /** 文件类型,可选,会根据文件扩展名自动判断 */ + type?: string; + /** 上传进度回调 */ + onProgress?: (progress: number) => void; +} + +/** + * 图像生成请求参数 + */ +interface GenerateImageParams { + /** 生成提示词 */ + prompt: string; + /** 模型名称,默认使用 gemini-2.5-flash-image-preview */ + model_name?: string; + /** 图像宽高比,如 '9:16', '16:9', '1:1' 等,默认为 '9:16' */ + aspect_ratio?: string; + /** 生成模式,默认为 turbo */ + mode?: 'turbo' | 'standard'; + /** 是否启用 webhook 回调,默认为 false */ + webhook_flag?: boolean; + img_file: string; +} + +export function isGetFileInfoSuccessCallbackResult(val: any): val is Taro.getFileInfo.SuccessCallbackResult { + return val && Reflect.has(val, 'digest') +} + +/** + * BowongAI SDK 类 + * 提供图像生成等 AI 功能的接口封装 + */ +export class BowongAISDK { + /** API 服务基础地址 */ + private readonly baseUrl = 'https://bowongai-test--text-video-agent-fastapi-app.modal.run'; + + /** + * 上传文件到 S3 + * 支持图片、视频等多种文件类型上传 + * @param params 上传参数 + * @returns Promise 返回上传后的文件地址 + */ + async upload(params: UploadParams): Promise { + const { filePath, name, type, onProgress } = params; + const url = `${this.baseUrl}/api/file/upload/s3`; + + try { + // 获取文件信息 + const fileInfo = await Taro.getFileInfo({ filePath }); + if (!isGetFileInfoSuccessCallbackResult(fileInfo)) { + throw new Error(fileInfo.errMsg) + } + // 自动生成文件名(如果未提供) + const fileName = name || `upload_${Date.now()}.${this._getFileExtension(filePath)}`; + + // 自动判断文件类型(如果未提供) + const fileType = type || this._getMimeType(filePath); + + console.log('开始上传文件:', { + fileName, + fileSize: fileInfo.size, + fileType + }); + + // 使用 Taro.uploadFile 进行文件上传 + const response = await new Promise((resolve, reject) => { + const uploadTask = Taro.uploadFile({ + url, + filePath, + name: 'file', // 服务端接收的字段名 + header: { + 'Accept': 'application/json' + }, + formData: { + filename: fileName, + type: fileType + }, + success: resolve, + fail: reject + }); + + // 监听上传进度 + if (onProgress) { + uploadTask.progress((res) => { + const progress = Math.round((res.totalBytesSent / res.totalBytesExpectedToSend) * 100); + onProgress(progress); + }); + } + }); + + // 检查 HTTP 状态码 + if (response.statusCode !== 200) { + throw new Error(`HTTP ${response.statusCode}: 文件上传失败`); + } + + // 解析响应数据 + let result: ApiResponse; + try { + result = JSON.parse(response.data) as ApiResponse; + } catch (parseError) { + throw new Error('服务器响应格式错误'); + } + + // 检查业务状态码 + if (!result.status) { + throw new Error(result.msg || '文件上传失败'); + } + + console.log('文件上传成功:', result.data); + return result.data; + + } catch (error) { + console.error('文件上传失败:', error); + throw error; + } + } + + /** + * 选择并上传图片 + * 封装了选择图片和上传的完整流程 + * @param options 选择图片的配置选项 + * @returns Promise 返回上传后的文件地址 + */ + async chooseAndGenerateImage(options?: { + /** 最多可以选择的图片张数,默认为1 */ + count?: number; + /** 所选的图片的尺寸,默认为 ['original', 'compressed'] */ + sizeType?: ('original' | 'compressed')[]; + /** 选择图片的来源,默认为 ['album', 'camera'] */ + sourceType?: ('album' | 'camera')[]; + }): Promise { + const { + count = 1, + sizeType = ['original', 'compressed'], + sourceType = ['album', 'camera'], + } = options || {}; + + try { + // 选择图片 + const chooseResult = await Taro.chooseImage({ + count, + sizeType, + sourceType + }); + + if (!chooseResult.tempFilePaths || chooseResult.tempFilePaths.length === 0) { + throw new Error('未选择图片'); + } + + // 上传第一张图片 + const filePath = chooseResult.tempFilePaths[0]; + return await this.generateImage({ + img_file: filePath, + prompt: `将画面中的角色重塑为顶级收藏级树脂手办,全身动态姿势,置于角色主题底座;高精度材质,手工涂装,肌肤纹理与服装材质真实分明。 +戏剧性硬光为主光源,凸显立体感,无过曝;强效补光消除死黑,细节完整可见。背景为窗边景深模糊,侧后方隐约可见产品包装盒。 +博物馆级摄影质感,全身细节无损,面部结构精准。禁止:任何2D元素或照搬原图、塑料感、面部模糊、五官错位、细节丢失` + }); + + } catch (error) { + console.error('选择并上传图片失败:', error); + throw error; + } + } + + /** + * 根据文件路径获取文件扩展名 + * @private + */ + private _getFileExtension(filePath: string): string { + const match = filePath.match(/\.([^.]+)$/); + return match ? match[1] : 'jpg'; + } + + /** + * 根据文件路径获取 MIME 类型 + * @private + */ + private _getMimeType(filePath: string): string { + const extension = this._getFileExtension(filePath).toLowerCase(); + const mimeTypes: Record = { + 'jpg': 'image/jpeg', + 'jpeg': 'image/jpeg', + 'png': 'image/png', + 'gif': 'image/gif', + 'webp': 'image/webp', + 'mp4': 'video/mp4', + 'mov': 'video/quicktime', + 'avi': 'video/x-msvideo' + }; + return mimeTypes[extension] || 'application/octet-stream'; + } + + /** + * 生成图像 + * @param params 图像生成参数 + * @returns Promise 返回任务ID + * + * 使用 multipart/form-data 格式提交,支持文件上传和URL参数 + * 对应 curl 命令: + * curl -X 'POST' \ + * 'https://bowongai-test--text-video-agent-fastapi-app.modal.run/api/custom/image/submit/task' \ + * -H 'accept: application/json' \ + * -H 'Content-Type: multipart/form-data' \ + * -F 'prompt=...' \ + * -F 'model_name=gemini-2.5-flash-image-preview' \ + * -F 'aspect_ratio=9:16' \ + * -F 'mode=turbo' \ + * -F 'webhook_flag=false' \ + * -F 'img_url=https://...' \ + * -F 'img_file=' + */ + async generateImage(params: GenerateImageParams): Promise { + const { + prompt, + model_name = 'gemini-2.5-flash-image-preview', + aspect_ratio = '9:16', + mode = 'turbo', + webhook_flag = false, + img_file + } = params; + + const url = `${this.baseUrl}/api/custom/image/submit/task`; + + try { + // 改为选择图片 + + // 如果提供了本地文件路径,使用 uploadFile + if (img_file) { + const response = await new Promise((resolve, reject) => { + Taro.uploadFile({ + url, + filePath: img_file, + name: 'img_file', + header: { + 'accept': 'application/json' + }, + formData: { + prompt, + model_name, + aspect_ratio, + mode: mode.toString(), + webhook_flag: webhook_flag.toString(), + img_url: '' + }, + success: resolve, + fail: reject + }); + }); + + if (response.statusCode !== 200) { + throw new Error(`HTTP ${response.statusCode}: 图像生成请求失败`); + } + + const result = JSON.parse(response.data) as ApiResponse; + + if (!result.status) { + throw new Error(result.msg || '图像生成请求失败'); + } + + console.log('图像生成任务提交成功:', result.data); + return result.data; + } + throw new Error(`请选择图片`) + } catch (error) { + console.error('图像生成失败:', error); + throw error; + } + } + + /** + * 查询任务状态 + * @param taskId 任务ID + * @param retryCount 内部重试计数,外部调用不需要传入 + * @returns Promise 返回任务状态信息 + */ + async getTaskStatus(taskId: string, retryCount: number = 0): Promise { + const url = `${this.baseUrl}/api/custom/task/status?task_id=${taskId}`; + const maxRetries = 3; + + try { + const response = await Taro.request({ + url, + method: 'GET' + }); + + if (response.statusCode !== 200) { + throw new Error(`HTTP ${response.statusCode}: 查询任务状态失败`); + } + + const result = response.data as ApiResponse; + + if (!result.status) { + throw new Error(result.msg || '查询任务状态失败'); + } + + if (result.status === 'running') { + await new Promise((resolve) => setTimeout(resolve, 1000 * 5)) + return this.getTaskStatus(taskId) + } + + return result.data; + + } catch (error) { + console.error(`查询任务状态失败 (第${retryCount + 1}次尝试):`, error); + + // 如果重试次数未达到最大值,等待5秒后重试 + if (retryCount < maxRetries) { + console.log(`将在5秒后进行第${retryCount + 2}次重试...`); + await new Promise((resolve) => setTimeout(resolve, 5000)); + return this.getTaskStatus(taskId, retryCount + 1); + } + + // 超过最大重试次数,抛出异常 + throw new Error(`查询任务状态失败,已重试${maxRetries}次: ${error instanceof Error ? error.message : String(error)}`); + } + } +} + +/** + * 使用示例: + * + * // 1. 选择并上传图片 + * const uploadResult = await bowongAI.chooseAndUploadImage({ + * onProgress: (progress) => console.log(`上传进度: ${progress}%`) + * }); + * + * // 2. 使用上传的图片生成新图像 + * const generateResult = await bowongAI.generateImage({ + * img_url: uploadResult.file_url, + * prompt: '转换为油画风格' + * }); + * + * // 3. 查询生成状态 + * const status = await bowongAI.getTaskStatus(generateResult.task_id); + * + * // 4. 获取最终结果 + * const finalResult = await bowongAI.getTaskResult(generateResult.task_id); + */ + +// 导出 SDK 实例 +export const bowongAI = new BowongAISDK(); + +// 向后兼容的类名导出 +export const Sdk = BowongAISDK; \ No newline at end of file