From fcd61c692a676471e366f8a0d742810f3e8ae2ba Mon Sep 17 00:00:00 2001 From: TheSmallHanCat Date: Mon, 1 Dec 2025 19:12:46 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=9B=BE=E7=89=87=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E5=A4=9A=E5=9B=BE=E6=94=AF=E6=8C=81=E3=80=81=E9=87=8D=E7=BD=AE?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E8=AE=A1=E6=95=B0=20feat:=20=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E7=B1=BB=E5=9E=8B=E5=88=86=E9=85=8D=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E9=85=8D=E9=A2=9D=20Fixes=20#5,#7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/api/admin.py | 2 +- src/core/models.py | 2 +- src/services/generation_handler.py | 34 ++++++++++++++++++------------ src/services/load_balancer.py | 12 +++++++++-- static/manage.html | 4 +++- 6 files changed, 36 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 36ec53d..4c97abd 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ ```bash # 克隆项目 git clone https://github.com/TheSmallHanCat/flow2api.git -cd sora2api +cd flow2api # 启动服务 docker-compose up -d diff --git a/src/api/admin.py b/src/api/admin.py index 9af9a69..805366c 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -499,7 +499,7 @@ async def get_stats(token: str = Depends(verify_admin_token)): if stats: total_images += stats.image_count total_videos += stats.video_count - total_errors += stats.error_count + total_errors += stats.error_count # Historical total errors today_images += stats.today_image_count today_videos += stats.today_video_count today_errors += stats.today_error_count diff --git a/src/core/models.py b/src/core/models.py index 9308572..0bca5f3 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -56,7 +56,7 @@ class TokenStats(BaseModel): image_count: int = 0 video_count: int = 0 success_count: int = 0 - error_count: int = 0 + error_count: int = 0 # Historical total errors (never reset) last_success_at: Optional[datetime] = None last_error_at: Optional[datetime] = None # 今日统计 diff --git a/src/services/generation_handler.py b/src/services/generation_handler.py index ee6984a..21d1c07 100644 --- a/src/services/generation_handler.py +++ b/src/services/generation_handler.py @@ -281,9 +281,9 @@ class GenerationHandler: debug_logger.log_info(f"[GENERATION] 正在选择可用Token...") if generation_type == "image": - token = await self.load_balancer.select_token(for_image_generation=True) + token = await self.load_balancer.select_token(for_image_generation=True, model=model) else: - token = await self.load_balancer.select_token(for_video_generation=True) + token = await self.load_balancer.select_token(for_video_generation=True, model=model) if not token: error_msg = self._get_no_token_error_message(generation_type) @@ -335,6 +335,10 @@ class GenerationHandler: # 6. 记录使用 is_video = (generation_type == "video") await self.token_manager.record_usage(token.id, is_video=is_video) + + # 重置错误计数 (请求成功时清空连续错误计数) + await self.token_manager.record_success(token.id) + debug_logger.log_info(f"[GENERATION] ✅ 生成成功完成") # 7. 记录成功日志 @@ -397,19 +401,21 @@ class GenerationHandler: image_inputs = [] if images and len(images) > 0: if stream: - yield self._create_stream_chunk("上传参考图片...\n") + yield self._create_stream_chunk(f"上传 {len(images)} 张参考图片...\n") - image_bytes = images[0] # 图生图只需要一张 - media_id = await self.flow_client.upload_image( - token.at, - image_bytes, - model_config["aspect_ratio"] - ) - - image_inputs = [{ - "name": media_id, - "imageInputType": "IMAGE_INPUT_TYPE_REFERENCE" - }] + # 支持多图输入 + for idx, image_bytes in enumerate(images): + media_id = await self.flow_client.upload_image( + token.at, + image_bytes, + model_config["aspect_ratio"] + ) + image_inputs.append({ + "name": media_id, + "imageInputType": "IMAGE_INPUT_TYPE_REFERENCE" + }) + if stream: + yield self._create_stream_chunk(f"已上传第 {idx + 1}/{len(images)} 张图片\n") # 调用生成API if stream: diff --git a/src/services/load_balancer.py b/src/services/load_balancer.py index ff043d0..dc61c0e 100644 --- a/src/services/load_balancer.py +++ b/src/services/load_balancer.py @@ -16,7 +16,8 @@ class LoadBalancer: async def select_token( self, for_image_generation: bool = False, - for_video_generation: bool = False + for_video_generation: bool = False, + model: Optional[str] = None ) -> Optional[Token]: """ Select a token using random load balancing @@ -24,11 +25,12 @@ class LoadBalancer: Args: for_image_generation: If True, only select tokens with image_enabled=True for_video_generation: If True, only select tokens with video_enabled=True + model: Model name (used to filter tokens for specific models) Returns: Selected token or None if no available tokens """ - debug_logger.log_info(f"[LOAD_BALANCER] 开始选择Token (图片生成={for_image_generation}, 视频生成={for_video_generation})") + debug_logger.log_info(f"[LOAD_BALANCER] 开始选择Token (图片生成={for_image_generation}, 视频生成={for_video_generation}, 模型={model})") active_tokens = await self.token_manager.get_active_tokens() debug_logger.log_info(f"[LOAD_BALANCER] 获取到 {len(active_tokens)} 个活跃Token") @@ -47,6 +49,12 @@ class LoadBalancer: filtered_reasons[token.id] = "AT无效或已过期" continue + # Filter for gemini-3.0 models (skip free tier tokens) + if model and model in ["gemini-3.0-pro-image-landscape", "gemini-3.0-pro-image-portrait"]: + if token.user_paygate_tier == "PAYGATE_TIER_NOT_PAID": + filtered_reasons[token.id] = "gemini-3.0模型不支持普通账号" + continue + # Filter for image generation if for_image_generation: if not token.image_enabled: diff --git a/static/manage.html b/static/manage.html index 3a488b5..e1919fb 100644 --- a/static/manage.html +++ b/static/manage.html @@ -134,6 +134,7 @@ 状态 过期时间 余额 + 类型 项目名称 项目ID 图片 @@ -536,7 +537,8 @@ formatSora2=(t)=>{if(t.sora2_supported===true){const remaining=t.sora2_total_count-t.sora2_redeemed_count;const tooltipText=`邀请码: ${t.sora2_invite_code||'无'}\n可用次数: ${remaining}/${t.sora2_total_count}\n已用次数: ${t.sora2_redeemed_count}`;return`
支持${remaining}/${t.sora2_total_count}
`}else if(t.sora2_supported===false){return`不支持`}else{return'-'}}, formatPlanTypeWithTooltip=(t)=>{const tooltipText=t.subscription_end?`套餐到期: ${new Date(t.subscription_end).toLocaleDateString('zh-CN',{year:'numeric',month:'2-digit',day:'2-digit'}).replace(/\//g,'-')} ${new Date(t.subscription_end).toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit',hour12:false})}`:'';return`${formatPlanType(t.plan_type)}`}, formatSora2Remaining=(t)=>{if(t.sora2_supported===true){const remaining=t.sora2_remaining_count||0;return`${remaining}`}else{return'-'}}, - renderTokens=()=>{const tb=$('tokenTableBody');tb.innerHTML=allTokens.map(t=>{const imageDisplay=t.image_enabled?`${t.image_count||0}`:'-';const videoDisplay=t.video_enabled?`${t.video_count||0}`:'-';const creditsDisplay=t.credits!==undefined?`${t.credits}`:'-';const projectDisplay=t.current_project_name||'-';const projectIdDisplay=t.current_project_id?(t.current_project_id.length>5?`${t.current_project_id.substring(0,5)}...`:`${t.current_project_id}`):'-';const expiryDisplay=formatExpiry(t.at_expires);return`${t.email}${t.is_active?'活跃':'禁用'}${expiryDisplay}${projectDisplay}${projectIdDisplay}${imageDisplay}${videoDisplay}${t.error_count||0}${t.remark||'-'}`}).join('')}, + formatAccountType=(tier)=>{if(tier==='PAYGATE_TIER_NOT_PAID'){return`普通`}else{return`会员`}}, + renderTokens=()=>{const tb=$('tokenTableBody');tb.innerHTML=allTokens.map(t=>{const imageDisplay=t.image_enabled?`${t.image_count||0}`:'-';const videoDisplay=t.video_enabled?`${t.video_count||0}`:'-';const creditsDisplay=t.credits!==undefined?`${t.credits}`:'-';const accountTypeDisplay=formatAccountType(t.user_paygate_tier);const projectDisplay=t.current_project_name||'-';const projectIdDisplay=t.current_project_id?(t.current_project_id.length>5?`${t.current_project_id.substring(0,5)}...`:`${t.current_project_id}`):'-';const expiryDisplay=formatExpiry(t.at_expires);return`${t.email}${t.is_active?'活跃':'禁用'}${expiryDisplay}${accountTypeDisplay}${projectDisplay}${projectIdDisplay}${imageDisplay}${videoDisplay}${t.error_count||0}${t.remark||'-'}`}).join('')}, refreshTokenCredits=async(id)=>{try{showToast('正在刷新余额...','info');const r=await apiRequest(`/api/tokens/${id}/refresh-credits`,{method:'POST'});if(!r)return;const d=await r.json();if(d.success){showToast(`余额刷新成功: ${d.credits}`,'success');await refreshTokens()}else{showToast('刷新失败: '+(d.detail||'未知错误'),'error')}}catch(e){showToast('刷新失败: '+e.message,'error')}}, refreshTokenAT=async(id)=>{try{showToast('正在更新AT...','info');const r=await apiRequest(`/api/tokens/${id}/refresh-at`,{method:'POST'});if(!r)return;const d=await r.json();if(d.success){const expiresDate=d.token.at_expires?new Date(d.token.at_expires):null;const expiresStr=expiresDate?expiresDate.toLocaleString('zh-CN',{year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',hour12:false}).replace(/\//g,'-'):'未知';showToast(`AT更新成功! 新过期时间: ${expiresStr}`,'success');await refreshTokens()}else{showToast('更新失败: '+(d.detail||'未知错误'),'error')}}catch(e){showToast('更新失败: '+e.message,'error')}}, refreshTokens=async()=>{await loadTokens();await loadStats()},