2026-04-24 23:17:10 +08:00
|
|
|
|
import json
|
|
|
|
|
|
import threading
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
|
|
|
|
|
|
import requests
|
|
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
|
from config import Config
|
2026-04-24 23:17:10 +08:00
|
|
|
|
from extensions import db, redis_client
|
|
|
|
|
|
from models import GenerationRecord, SystemDict, User
|
2026-01-17 23:15:58 +08:00
|
|
|
|
from services.logger import system_logger
|
|
|
|
|
|
from services.task_service import process_image_generation, process_video_generation
|
2026-04-22 11:53:00 +08:00
|
|
|
|
from utils import get_api_candidates, get_proxied_url, should_switch_to_backup
|
2026-01-17 23:15:58 +08:00
|
|
|
|
|
2026-04-24 23:17:10 +08:00
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
|
def get_model_cost(model_value, is_video=False):
|
2026-04-24 23:17:10 +08:00
|
|
|
|
"""获取模型消耗积分。"""
|
|
|
|
|
|
dict_type = "video_model" if is_video else "ai_model"
|
2026-01-17 23:15:58 +08:00
|
|
|
|
model_dict = SystemDict.query.filter_by(dict_type=dict_type, value=model_value).first()
|
2026-04-24 23:17:10 +08:00
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
|
if model_dict:
|
|
|
|
|
|
return model_dict.cost
|
2026-04-24 23:17:10 +08:00
|
|
|
|
|
|
|
|
|
|
# 默认计费兜底
|
2026-01-17 23:15:58 +08:00
|
|
|
|
if is_video:
|
|
|
|
|
|
return 15 if "pro" in model_value.lower() or "3.1" in model_value else 10
|
2026-04-24 23:17:10 +08:00
|
|
|
|
return 1
|
|
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
|
|
|
|
|
|
|
def validate_generation_request(user, data):
|
2026-04-24 23:17:10 +08:00
|
|
|
|
"""校验生图请求并返回 (api_key, target_api, cost, use_trial, error)。"""
|
|
|
|
|
|
mode = data.get("mode", "trial")
|
|
|
|
|
|
is_premium = data.get("is_premium", False)
|
|
|
|
|
|
input_key = data.get("apiKey")
|
|
|
|
|
|
model_value = data.get("model")
|
|
|
|
|
|
is_gemini_flash_image_preview = model_value == "gemini-3.1-flash-image-preview"
|
|
|
|
|
|
is_gpt_image_edit = model_value == "gpt-image-2"
|
2026-04-13 11:50:32 +08:00
|
|
|
|
|
|
|
|
|
|
if isinstance(is_premium, str):
|
2026-04-24 23:17:10 +08:00
|
|
|
|
is_premium = is_premium.lower() in ("1", "true", "yes", "on")
|
|
|
|
|
|
|
2026-05-14 12:30:10 +08:00
|
|
|
|
# gpt-image-2 统一走 AI_BASE_URL 下的 generations 接口,参考图放在 image 数组中。
|
2026-04-26 09:49:31 +08:00
|
|
|
|
target_api = (
|
2026-05-14 12:30:10 +08:00
|
|
|
|
Config.GPT_IMAGE_GENERATION_API
|
2026-04-26 09:49:31 +08:00
|
|
|
|
if is_gpt_image_edit
|
|
|
|
|
|
else Config.AI_API
|
|
|
|
|
|
)
|
2026-01-17 23:15:58 +08:00
|
|
|
|
api_key = None
|
|
|
|
|
|
use_trial = False
|
2026-04-24 23:17:10 +08:00
|
|
|
|
|
|
|
|
|
|
if mode == "key":
|
2026-01-17 23:15:58 +08:00
|
|
|
|
api_key = input_key or user.api_key
|
|
|
|
|
|
if not api_key:
|
|
|
|
|
|
return None, None, 0, False, "请先输入您的 API 密钥"
|
2026-04-24 23:17:10 +08:00
|
|
|
|
|
|
|
|
|
|
# 用户手动输入了新 key 时,同步保存到账号
|
2026-01-17 23:15:58 +08:00
|
|
|
|
if input_key and input_key != user.api_key:
|
|
|
|
|
|
user.api_key = input_key
|
|
|
|
|
|
db.session.commit()
|
|
|
|
|
|
else:
|
2026-04-24 23:17:10 +08:00
|
|
|
|
if user.points <= 0:
|
2026-01-17 23:15:58 +08:00
|
|
|
|
return None, None, 0, False, "可用积分已耗尽,请充值或切换至自定义 Key 模式"
|
2026-04-24 23:17:10 +08:00
|
|
|
|
|
2026-05-14 12:30:10 +08:00
|
|
|
|
# 积分模式下,gpt-image-2 跟随 AI_BASE_URL 的内置高级线路。
|
2026-04-24 23:17:10 +08:00
|
|
|
|
if is_gpt_image_edit:
|
2026-05-14 12:30:10 +08:00
|
|
|
|
api_key = Config.PREMIUM_KEY
|
2026-04-24 23:17:10 +08:00
|
|
|
|
elif is_premium:
|
|
|
|
|
|
api_key = (
|
|
|
|
|
|
Config.GEMINI_FLASH_IMAGE_PREMIUM_KEY
|
|
|
|
|
|
if is_gemini_flash_image_preview
|
|
|
|
|
|
else Config.PREMIUM_KEY
|
|
|
|
|
|
)
|
|
|
|
|
|
elif is_gemini_flash_image_preview:
|
|
|
|
|
|
api_key = Config.PREMIUM_KEY
|
|
|
|
|
|
else:
|
|
|
|
|
|
api_key = Config.TRIAL_KEY
|
|
|
|
|
|
|
2026-04-26 09:49:31 +08:00
|
|
|
|
target_api = (
|
2026-05-14 12:30:10 +08:00
|
|
|
|
Config.GPT_IMAGE_GENERATION_API
|
2026-04-26 09:49:31 +08:00
|
|
|
|
if is_gpt_image_edit
|
|
|
|
|
|
else Config.TRIAL_API
|
|
|
|
|
|
)
|
2026-04-24 23:17:10 +08:00
|
|
|
|
use_trial = True
|
|
|
|
|
|
|
|
|
|
|
|
# 计算本次任务消耗
|
2026-01-20 09:53:06 +08:00
|
|
|
|
cost = get_model_cost(model_value, is_video=False)
|
2026-01-17 23:15:58 +08:00
|
|
|
|
if use_trial and is_premium:
|
2026-01-20 09:53:06 +08:00
|
|
|
|
cost *= 2
|
2026-04-24 23:17:10 +08:00
|
|
|
|
|
|
|
|
|
|
if use_trial and user.points < cost:
|
|
|
|
|
|
return None, None, cost, True, f"可用积分不足,本次需要 {cost} 积分"
|
|
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
|
return api_key, target_api, cost, use_trial, None
|
|
|
|
|
|
|
2026-04-24 23:17:10 +08:00
|
|
|
|
|
2026-01-20 09:53:06 +08:00
|
|
|
|
def deduct_points(user_id, cost):
|
2026-04-24 23:17:10 +08:00
|
|
|
|
"""原子扣减积分。"""
|
2026-01-20 09:53:06 +08:00
|
|
|
|
user = db.session.query(User).filter_by(id=user_id).populate_existing().with_for_update().first()
|
|
|
|
|
|
if user:
|
|
|
|
|
|
user.points -= cost
|
|
|
|
|
|
user.has_used_points = True
|
|
|
|
|
|
db.session.commit()
|
2026-01-17 23:15:58 +08:00
|
|
|
|
|
2026-04-24 23:17:10 +08:00
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
|
def refund_points(user_id, cost):
|
2026-04-24 23:17:10 +08:00
|
|
|
|
"""原子退回积分。"""
|
2026-01-17 23:15:58 +08:00
|
|
|
|
try:
|
2026-01-20 09:53:06 +08:00
|
|
|
|
user = db.session.query(User).filter_by(id=user_id).populate_existing().with_for_update().first()
|
2026-01-17 23:15:58 +08:00
|
|
|
|
if user:
|
|
|
|
|
|
user.points += cost
|
|
|
|
|
|
db.session.commit()
|
2026-04-24 23:17:10 +08:00
|
|
|
|
except Exception:
|
2026-01-20 09:53:06 +08:00
|
|
|
|
db.session.rollback()
|
2026-01-17 23:15:58 +08:00
|
|
|
|
|
2026-04-24 23:17:10 +08:00
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
|
def handle_chat_generation_sync(user_id, api_key, model_value, prompt, use_trial, cost):
|
2026-04-24 23:17:10 +08:00
|
|
|
|
"""同步处理聊天类模型。"""
|
2026-01-17 23:15:58 +08:00
|
|
|
|
chat_payload = {
|
|
|
|
|
|
"model": model_value,
|
2026-04-24 23:17:10 +08:00
|
|
|
|
"messages": [{"role": "user", "content": prompt}],
|
2026-01-17 23:15:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
try:
|
2026-04-22 11:53:00 +08:00
|
|
|
|
resp = None
|
|
|
|
|
|
last_error = None
|
|
|
|
|
|
candidates = get_api_candidates(Config.CHAT_API, api_key)
|
|
|
|
|
|
|
|
|
|
|
|
for idx, candidate in enumerate(candidates):
|
2026-04-24 23:17:10 +08:00
|
|
|
|
headers = {
|
|
|
|
|
|
"Authorization": f"Bearer {candidate['api_key']}",
|
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
|
}
|
2026-04-22 11:53:00 +08:00
|
|
|
|
try:
|
|
|
|
|
|
resp = requests.post(
|
2026-04-24 23:17:10 +08:00
|
|
|
|
get_proxied_url(candidate["url"]),
|
2026-04-22 11:53:00 +08:00
|
|
|
|
json=chat_payload,
|
|
|
|
|
|
headers=headers,
|
2026-04-24 23:17:10 +08:00
|
|
|
|
timeout=Config.PROXY_TIMEOUT_LONG,
|
2026-04-22 11:53:00 +08:00
|
|
|
|
)
|
|
|
|
|
|
if resp.status_code == 200:
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
last_error = resp.text
|
|
|
|
|
|
has_backup = idx < len(candidates) - 1
|
|
|
|
|
|
if has_backup and should_switch_to_backup(resp.status_code):
|
|
|
|
|
|
system_logger.warning(
|
|
|
|
|
|
"聊天主线路失败,切换备用线路",
|
|
|
|
|
|
user_id=user_id,
|
|
|
|
|
|
model=model_value,
|
2026-04-24 23:17:10 +08:00
|
|
|
|
status_code=resp.status_code,
|
2026-04-22 11:53:00 +08:00
|
|
|
|
)
|
|
|
|
|
|
continue
|
|
|
|
|
|
break
|
|
|
|
|
|
except requests.RequestException as e:
|
|
|
|
|
|
last_error = str(e)
|
|
|
|
|
|
if idx < len(candidates) - 1:
|
|
|
|
|
|
system_logger.warning(
|
|
|
|
|
|
"聊天请求异常,切换备用线路",
|
|
|
|
|
|
user_id=user_id,
|
|
|
|
|
|
model=model_value,
|
2026-04-24 23:17:10 +08:00
|
|
|
|
error=last_error,
|
2026-04-22 11:53:00 +08:00
|
|
|
|
)
|
|
|
|
|
|
continue
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
if not resp or resp.status_code != 200:
|
2026-01-17 23:15:58 +08:00
|
|
|
|
if use_trial:
|
|
|
|
|
|
refund_points(user_id, cost)
|
2026-04-22 11:53:00 +08:00
|
|
|
|
return {"error": last_error or "聊天请求失败"}, resp.status_code if resp else 500
|
2026-04-24 23:17:10 +08:00
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
|
api_result = resp.json()
|
2026-04-24 23:17:10 +08:00
|
|
|
|
content = api_result["choices"][0]["message"]["content"]
|
|
|
|
|
|
|
|
|
|
|
|
# 非验光单解读的文本生成写入历史
|
2026-01-17 23:15:58 +08:00
|
|
|
|
if prompt != "解读验光单":
|
|
|
|
|
|
new_record = GenerationRecord(
|
|
|
|
|
|
user_id=user_id,
|
|
|
|
|
|
prompt=prompt,
|
|
|
|
|
|
model=model_value,
|
|
|
|
|
|
cost=cost,
|
2026-04-24 23:17:10 +08:00
|
|
|
|
image_urls=json.dumps([{"type": "text", "content": content}]),
|
2026-01-17 23:15:58 +08:00
|
|
|
|
)
|
|
|
|
|
|
db.session.add(new_record)
|
|
|
|
|
|
db.session.commit()
|
2026-04-24 23:17:10 +08:00
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
|
return {
|
|
|
|
|
|
"data": [{"content": content, "type": "text"}],
|
2026-04-24 23:17:10 +08:00
|
|
|
|
"message": "生成成功",
|
2026-01-17 23:15:58 +08:00
|
|
|
|
}, 200
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
if use_trial:
|
|
|
|
|
|
refund_points(user_id, cost)
|
|
|
|
|
|
return {"error": str(e)}, 500
|
|
|
|
|
|
|
2026-04-24 23:17:10 +08:00
|
|
|
|
|
2026-01-20 16:01:58 +08:00
|
|
|
|
def start_async_image_task(app, user_id, payload, api_key, target_api, cost, mode, model_value, use_trial=False):
|
2026-04-24 23:17:10 +08:00
|
|
|
|
"""启动异步生图任务。"""
|
2026-01-17 23:15:58 +08:00
|
|
|
|
task_id = str(uuid.uuid4())
|
2026-04-24 23:17:10 +08:00
|
|
|
|
|
|
|
|
|
|
log_msg = "用户发起验光单解读" if payload.get("prompt") == "解读验光单" else "用户发起生图任务"
|
2026-01-17 23:15:58 +08:00
|
|
|
|
system_logger.info(log_msg, model=model_value, mode=mode)
|
2026-04-24 23:17:10 +08:00
|
|
|
|
|
|
|
|
|
|
redis_client.setex(
|
|
|
|
|
|
f"task:{task_id}",
|
|
|
|
|
|
3600,
|
|
|
|
|
|
json.dumps({"status": "queued", "message": "任务已提交,等待处理..."}),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
|
threading.Thread(
|
|
|
|
|
|
target=process_image_generation,
|
2026-04-24 23:17:10 +08:00
|
|
|
|
args=(app, user_id, task_id, payload, api_key, target_api, cost, use_trial),
|
2026-01-17 23:15:58 +08:00
|
|
|
|
).start()
|
2026-04-24 23:17:10 +08:00
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
|
return task_id
|
|
|
|
|
|
|
2026-04-24 23:17:10 +08:00
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
|
def validate_video_request(user, data):
|
2026-04-24 23:17:10 +08:00
|
|
|
|
"""校验视频生成请求。"""
|
2026-01-17 23:15:58 +08:00
|
|
|
|
if user.points <= 0:
|
|
|
|
|
|
return None, 0, "可用积分不足,请先充值"
|
2026-04-24 23:17:10 +08:00
|
|
|
|
|
|
|
|
|
|
model_value = data.get("model", "veo3.1")
|
2026-01-17 23:15:58 +08:00
|
|
|
|
cost = get_model_cost(model_value, is_video=True)
|
2026-04-24 23:17:10 +08:00
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
|
if user.points < cost:
|
|
|
|
|
|
return None, cost, f"积分不足,生成该视频需要 {cost} 积分"
|
2026-04-24 23:17:10 +08:00
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
|
return model_value, cost, None
|
|
|
|
|
|
|
2026-04-24 23:17:10 +08:00
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
|
def start_async_video_task(app, user_id, payload, cost, model_value):
|
2026-04-24 23:17:10 +08:00
|
|
|
|
"""启动异步视频任务。"""
|
2026-04-22 11:53:00 +08:00
|
|
|
|
api_key = Config.VIDEO_KEY
|
2026-01-17 23:15:58 +08:00
|
|
|
|
task_id = str(uuid.uuid4())
|
2026-04-24 23:17:10 +08:00
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
|
system_logger.info("用户发起视频生成任务 (积分模式)", model=model_value, cost=cost)
|
2026-04-24 23:17:10 +08:00
|
|
|
|
|
|
|
|
|
|
redis_client.setex(
|
|
|
|
|
|
f"task:{task_id}",
|
|
|
|
|
|
3600,
|
|
|
|
|
|
json.dumps({"status": "queued", "message": "视频任务已提交,准备开始处理..."}),
|
|
|
|
|
|
)
|
2026-01-23 18:01:54 +08:00
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
|
threading.Thread(
|
|
|
|
|
|
target=process_video_generation,
|
2026-04-24 23:17:10 +08:00
|
|
|
|
args=(app, user_id, task_id, payload, api_key, cost, True),
|
2026-01-17 23:15:58 +08:00
|
|
|
|
).start()
|
2026-04-24 23:17:10 +08:00
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
|
return task_id
|