✨ feat: 完整的生成流程 - 登录检查、文件上传、支付凭证验证
主要功能: 1. ✅ 添加 canGenerate() - 检查登录、metered 订阅、余额 2. ✅ 添加 uploadBlobFiles() - 自动上传所有 blob URL 到服务器 3. ✅ 支付凭证强制验证 - identifier 缺失时报错"支付失败" 4. ✅ 新增 lib/api/upload.ts - 文件上传 API 5. ✅ 更新 useTemplateRun hook 支持 identifier 参数 完整流程(7步骤): 1. 表单验证 2. canGenerate 检查(登录 + 订阅 + 余额) 3. 上传 blob 文件到服务器 4. recordTokenUsage 扣费并获取 identifier 5. 验证所有 URL 都是服务器 URL 6. 转换数据格式 7. runTemplate(id, data, identifier) 创建任务 安全保障: - ❌ 无 identifier → 抛出错误"支付凭证缺失" - ❌ blob URL 未上传 → 报错"存在未上传的文件" - ❌ 余额不足 → 引导用户充值 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,19 +1,14 @@
|
||||
import { apiClient } from './client';
|
||||
import { RunTemplateResponse, TemplateGenerationResponse, RunTemplateData } from '../types/template-run';
|
||||
import { storage } from '../storage';
|
||||
|
||||
export async function runTemplate(templateId: string, data: RunTemplateData): Promise<RunTemplateResponse> {
|
||||
// 从 storage 获取用户 session 作为 identifier
|
||||
const sessionStr = await storage.getItem('session');
|
||||
let identifier = 'anonymous';
|
||||
|
||||
if (sessionStr) {
|
||||
try {
|
||||
const session = JSON.parse(sessionStr);
|
||||
identifier = session?.user?.id || 'anonymous';
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse session:', error);
|
||||
}
|
||||
export async function runTemplate(
|
||||
templateId: string,
|
||||
data: RunTemplateData,
|
||||
identifier: string
|
||||
): Promise<RunTemplateResponse> {
|
||||
// identifier 是必需的支付凭证,如果没有提供说明支付失败
|
||||
if (!identifier) {
|
||||
throw new Error('支付凭证缺失,无法创建生成任务');
|
||||
}
|
||||
|
||||
return apiClient<RunTemplateResponse>(`/api/templates/${templateId}/run`, {
|
||||
|
||||
88
lib/api/upload.ts
Normal file
88
lib/api/upload.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { apiClient } from './client';
|
||||
|
||||
export interface UploadResponse {
|
||||
success: boolean;
|
||||
data?: {
|
||||
url: string;
|
||||
filename: string;
|
||||
size: number;
|
||||
mimeType: string;
|
||||
};
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件到服务器
|
||||
* @param uri 本地文件 URI(可以是 blob: 或 file:// 格式)
|
||||
* @param type 文件类型(image 或 video)
|
||||
*/
|
||||
export async function uploadFile(uri: string, type: 'image' | 'video'): Promise<UploadResponse> {
|
||||
try {
|
||||
// 如果已经是 https URL,直接返回
|
||||
if (uri.startsWith('https://') || uri.startsWith('http://')) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
url: uri,
|
||||
filename: uri.split('/').pop() || 'file',
|
||||
size: 0,
|
||||
mimeType: type === 'image' ? 'image/jpeg' : 'video/mp4',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 创建 FormData
|
||||
const formData = new FormData();
|
||||
|
||||
// 从 URI 中提取文件信息
|
||||
const filename = uri.split('/').pop() || `${type}_${Date.now()}`;
|
||||
const match = /\.(\w+)$/.exec(filename);
|
||||
const fileType = match ? match[1] : (type === 'image' ? 'jpg' : 'mp4');
|
||||
|
||||
// 添加文件到 FormData
|
||||
formData.append('file', {
|
||||
uri,
|
||||
type: type === 'image' ? `image/${fileType}` : `video/${fileType}`,
|
||||
name: filename,
|
||||
} as any);
|
||||
|
||||
formData.append('type', type);
|
||||
|
||||
// 上传文件
|
||||
const response = await fetch(`${process.env.EXPO_PUBLIC_API_URL || 'https://api.mixvideo.bowong.cc'}/api/upload`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Upload failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || 'Upload failed');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Failed to upload file:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '上传失败,请稍后重试',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量上传文件
|
||||
*/
|
||||
export async function uploadFiles(
|
||||
files: Array<{ uri: string; type: 'image' | 'video' }>
|
||||
): Promise<UploadResponse[]> {
|
||||
const uploadPromises = files.map(file => uploadFile(file.uri, file.type));
|
||||
return Promise.all(uploadPromises);
|
||||
}
|
||||
Reference in New Issue
Block a user