feat: 升级图像生成SDK支持multipart/form-data格式

- 升级generateImage方法支持multipart/form-data提交格式
- 添加aspect_ratio参数,默认9:16比例
- 更新默认模型为gemini-2.5-flash-image-preview
- 为getTaskStatus方法添加失败重试机制,最多重试3次,每次间隔5秒
- 新增useSdk hook封装SDK使用
- 更新示例页面集成图像生成功能
This commit is contained in:
imeepos 2025-09-01 14:47:07 +08:00
parent 9b3bc7bf2d
commit 77ccaf8acd
5 changed files with 385 additions and 8 deletions

4
src/hooks/index.ts Normal file
View File

@ -0,0 +1,4 @@
export { useAd } from './useAd'
export { useSdk } from './useSdk'

View File

@ -8,7 +8,7 @@ interface UseAdReturn {
}
export function useAd(): UseAdReturn {
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(true);
const adRef = useRef<RewardedVideoAd | null>(null);
useEffect(() => {

6
src/hooks/useSdk.ts Normal file
View File

@ -0,0 +1,6 @@
import { bowongAI } from "../sdk";
export function useSdk() {
return bowongAI;
}

View File

@ -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 (
<View className='index'>
<Text>Hello world Ymm !!!!</Text>
<Button onClick={() => {
showAd()
}}
>广</Button>
sdk.chooseAndGenerateImage().then(task_id => {
console.log({ task_id })
return sdk.getTaskStatus(task_id)
}).then(res => {
console.log(res)
})
}}>
</Button>
</View>
)
}

362
src/sdk/index.ts Normal file
View File

@ -0,0 +1,362 @@
import Taro from "@tarojs/taro"
/**
* API
*/
interface ApiResponse<T = any> {
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<string>
*/
async upload(params: UploadParams): Promise<string> {
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<Taro.uploadFile.SuccessCallbackResult>((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<string>;
try {
result = JSON.parse(response.data) as ApiResponse<string>;
} 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<string>
*/
async chooseAndGenerateImage(options?: {
/** 最多可以选择的图片张数默认为1 */
count?: number;
/** 所选的图片的尺寸,默认为 ['original', 'compressed'] */
sizeType?: ('original' | 'compressed')[];
/** 选择图片的来源,默认为 ['album', 'camera'] */
sourceType?: ('album' | 'camera')[];
}): Promise<string> {
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<string, string> = {
'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<string> 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<string> {
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<Taro.uploadFile.SuccessCallbackResult>((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<string>;
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<any>
*/
async getTaskStatus(taskId: string, retryCount: number = 0): Promise<any> {
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;