fix:优化当前的项目

This commit is contained in:
Daniel
2026-04-28 18:36:38 +08:00
parent 04f26bdaaf
commit f47453a656
22 changed files with 3671 additions and 89 deletions

View File

@@ -1,6 +1,11 @@
from __future__ import annotations
import logging
import math
import re
import secrets
import time
import uuid
from pathlib import Path
from urllib.parse import urlparse
@@ -16,8 +21,12 @@ from app.middleware import RequestContextMiddleware
from app.schemas import (
AIModelCreateRequest,
AIModelDeleteRequest,
AIImageModelUpdateRequest,
AIModelSwitchRequest,
AuthCredentialRequest,
BillingRechargeCreateRequest,
BillingPayNowRequest,
BillingRechargeNotifyRequest,
ChangePasswordRequest,
DeleteAccountRequest,
ForgotPasswordResetRequest,
@@ -30,6 +39,7 @@ from app.schemas import (
WechatBindingRequest,
WechatPublishRequest,
WechatSwitchRequest,
UserProfileUpdateRequest,
VipRechargeRequest,
VipToggleRequest,
)
@@ -64,6 +74,11 @@ wechat = WechatPublisher()
poster_material = PosterMaterialService(wechat)
im = IMPublisher()
users = UserStore(settings.auth_db_path)
_register_rate: dict[str, list[float]] = {}
_login_rate: dict[str, list[float]] = {}
_challenge_pool: dict[str, dict] = {}
USERNAME_RE = re.compile(r"^[A-Za-z0-9_]{4,24}$")
PASSWORD_STRONG_RE = re.compile(r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9])\S{10,64}$")
def _session_ttl(remember_me: bool) -> int:
@@ -84,6 +99,67 @@ def _require_user(request: Request) -> dict | None:
return u
def _client_ip(request: Request) -> str:
xff = (request.headers.get("x-forwarded-for") or "").strip()
if xff:
return xff.split(",")[0].strip()
return (request.client.host if request.client else "") or "unknown"
def _hit_limit(bucket: dict[str, list[float]], key: str, limit: int, window_sec: int) -> bool:
now = time.time()
points = bucket.get(key, [])
points = [p for p in points if now - p <= window_sec]
points.append(now)
bucket[key] = points
return len(points) > limit
def _create_challenge() -> dict:
a = secrets.randbelow(8) + 2
b = secrets.randbelow(9) + 1
op = "+" if secrets.randbelow(2) == 0 else "-"
if op == "-":
a, b = max(a, b) + 5, min(a, b)
answer = str(a + b) if op == "+" else str(a - b)
cid = secrets.token_urlsafe(16)
_challenge_pool[cid] = {"answer": answer, "created_at": time.time(), "used": False}
return {"challenge_id": cid, "question": f"{a} {op} {b} = ?"}
def _verify_challenge(req: AuthCredentialRequest) -> tuple[bool, str]:
cid = (req.challenge_id or "").strip()
ans = (req.challenge_answer or "").strip()
if req.honeypot:
return False, "请求被拒绝"
if not cid or not ans:
return False, "请先完成人机校验"
item = _challenge_pool.get(cid)
if not item:
return False, "校验已失效,请刷新后重试"
if item.get("used"):
return False, "校验已使用,请刷新后重试"
age = time.time() - float(item.get("created_at") or 0)
if age < 2:
return False, "提交过快,请稍后重试"
if age > 300:
return False, "校验已过期,请刷新后重试"
if ans != str(item.get("answer") or ""):
return False, "人机校验答案错误"
item["used"] = True
return True, ""
def _validate_username_password(username: str, password: str) -> tuple[bool, str]:
if not USERNAME_RE.match(username):
return False, "用户名需为 4-24 位,仅支持字母/数字/下划线"
if not PASSWORD_STRONG_RE.match(password):
return False, "密码需 10-64 位,且包含大小写字母、数字和特殊字符"
if username.lower() in password.lower():
return False, "密码不能包含用户名"
return True, ""
def _platform_model_cfg() -> dict:
return {
"api_key": settings.platform_openai_api_key or "",
@@ -99,7 +175,9 @@ def _platform_model_cfg() -> dict:
def _select_model_cfg(user_id: int, prefer_vip: bool = True) -> tuple[dict | None, str]:
vip = users.get_vip_status(user_id)
if prefer_vip and vip.get("vip_enabled") and int(vip.get("token_balance") or 0) > 0:
if prefer_vip and vip.get("vip_enabled"):
if int(vip.get("total_available_credits") or 0) <= 0:
return None, "vip_empty"
cfg = _platform_model_cfg()
if cfg.get("api_key"):
return cfg, "vip"
@@ -107,12 +185,114 @@ def _select_model_cfg(user_id: int, prefer_vip: bool = True) -> tuple[dict | Non
return cfg, "user"
def _quota_detail() -> str:
return "Credits 额度已用完(席位额度+共享加油包),请充值或等待下个计费周期"
def _credits_from_cny(amount_cny: float) -> int:
package_credits = max(1, int(settings.credits_recharge_package_credits))
package_amount = max(0.01, float(settings.credits_recharge_package_amount))
cny = max(0.0, float(amount_cny))
if cny <= 0:
return 0
return max(1, int(math.ceil((cny * package_credits) / package_amount)))
def _estimate_rewrite_cost(req: RewriteRequest, result) -> int:
trace = getattr(result, "trace", None) or {}
usage = trace.get("usage") if isinstance(trace, dict) else {}
usage_total_tokens = int((usage or {}).get("total_tokens") or 0)
if usage_total_tokens > 0:
token_price_cny = max(0.0, float(settings.credits_token_price_per_million_cny))
return _credits_from_cny((usage_total_tokens / 1_000_000.0) * token_price_cny)
src_chars = len((req.source_text or "").strip())
out_chars = len((result.body_markdown or "").strip()) + len((result.title or "").strip()) + len((result.summary or "").strip())
total_chars = max(1, src_chars + out_chars)
blocks = (total_chars + 999) // 1000
return int(blocks * max(1, int(settings.vip_rewrite_token_per_1k_chars)))
estimated_tokens = int(total_chars * 1.8)
token_price_cny = max(0.0, float(settings.credits_token_price_per_million_cny))
return _credits_from_cny((max(1, estimated_tokens) / 1_000_000.0) * token_price_cny)
def _estimate_image_cost(image_count: int) -> int:
cnt = max(0, int(image_count))
if cnt <= 0:
return 0
pkg_images = max(1, int(settings.credits_image_price_package_images or 160))
pkg_cny = max(0.0, float(settings.credits_image_price_package_cny or 0.75))
return _credits_from_cny((cnt / pkg_images) * pkg_cny)
def _new_order_no() -> str:
return f"RC{time.strftime('%Y%m%d%H%M%S')}{uuid.uuid4().hex[:10].upper()}"
async def _create_recharge_order_with_pay_url(
user_id: int,
username: str,
credits: int,
callback_url: str,
order_meta: dict | None = None,
channel: str = "wechat",
) -> tuple[dict, str]:
package_credits = max(1, int(settings.credits_recharge_package_credits))
package_amount = float(settings.credits_recharge_package_amount)
amount_cny = round((int(credits) / package_credits) * package_amount, 2)
order_no = _new_order_no()
order = users.create_recharge_order(
user_id=user_id,
order_no=order_no,
channel=(channel or "wechat").strip() or "wechat",
token_amount=int(credits),
amount_cny=amount_cny,
meta={"username": username, **(order_meta or {})},
)
pay_payload = {
"order_no": order_no,
"user_id": int(user_id),
"username": username,
"callback_url": callback_url,
**order,
}
pay_url = ""
if settings.shop_backend_create_order_url:
try:
async with httpx.AsyncClient(timeout=15) as client:
r = await client.post(
settings.shop_backend_create_order_url,
json=pay_payload,
headers={"X-Shop-Token": settings.shop_backend_callback_token or ""},
)
body = r.json() if r.content else {}
if r.status_code < 400 and isinstance(body, dict):
pay_url = str(body.get("pay_url") or "")
except Exception as exc:
logger.warning("shop_create_order_failed order=%s err=%s", order_no, exc)
return order, pay_url
async def _fetch_pay_url_for_order(order: dict, user_id: int, username: str, callback_url: str) -> str:
pay_payload = {
"order_no": order.get("order_no", ""),
"user_id": int(user_id),
"username": username,
"callback_url": callback_url,
**order,
}
pay_url = ""
if settings.shop_backend_create_order_url:
try:
async with httpx.AsyncClient(timeout=15) as client:
r = await client.post(
settings.shop_backend_create_order_url,
json=pay_payload,
headers={"X-Shop-Token": settings.shop_backend_callback_token or ""},
)
body = r.json() if r.content else {}
if r.status_code < 400 and isinstance(body, dict):
pay_url = str(body.get("pay_url") or "")
except Exception as exc:
logger.warning("shop_repay_order_failed order=%s err=%s", order.get("order_no", ""), exc)
return pay_url
@app.get("/", response_class=HTMLResponse)
@@ -140,6 +320,48 @@ async def settings_page(request: Request):
return templates.TemplateResponse("settings.html", {"request": request, "app_name": settings.app_name})
@app.get("/billing", response_class=HTMLResponse)
async def billing_page(request: Request):
if not _current_user(request):
return RedirectResponse(url="/auth?next=/billing", status_code=302)
return templates.TemplateResponse(
"billing.html",
{
"request": request,
"app_name": settings.app_name,
"package_amount": settings.credits_recharge_package_amount,
"package_credits": settings.credits_recharge_package_credits,
},
)
@app.get("/upgrade", response_class=HTMLResponse)
async def upgrade_page(request: Request):
if not _current_user(request):
return RedirectResponse(url="/auth?next=/upgrade", status_code=302)
return templates.TemplateResponse(
"upgrade.html",
{
"request": request,
"app_name": settings.app_name,
"trial_tokens": settings.vip_trial_tokens,
"rewrite_cost": settings.credits_per_million_tokens,
"image_cost": settings.credits_per_120_images,
"seat_price": settings.credits_standard_seat_price_cny,
"seat_quota": settings.credits_seat_monthly_quota,
"package_amount": settings.credits_recharge_package_amount,
"package_credits": settings.credits_recharge_package_credits,
},
)
@app.get("/profile", response_class=HTMLResponse)
async def profile_page(request: Request):
if not _current_user(request):
return RedirectResponse(url="/auth?next=/profile", status_code=302)
return templates.TemplateResponse("profile.html", {"request": request, "app_name": settings.app_name})
@app.get("/guide", response_class=HTMLResponse)
async def guide_page(request: Request):
if not _current_user(request):
@@ -197,14 +419,48 @@ async def auth_me(request: Request):
}
@app.get("/api/profile")
async def api_profile(request: Request):
user = _require_user(request)
if not user:
return {"ok": False, "detail": "请先登录"}
return {"ok": True, "profile": users.get_user_profile(user["id"])}
@app.post("/api/profile")
async def api_profile_update(req: UserProfileUpdateRequest, request: Request):
user = _require_user(request)
if not user:
return {"ok": False, "detail": "请先登录"}
profile = users.save_user_profile(
user["id"],
subscriber_name=(req.subscriber_name or "").strip(),
subscriber_phone=(req.subscriber_phone or "").strip(),
shipping_address=(req.shipping_address or "").strip(),
)
return {"ok": True, "detail": "个人信息已保存", "profile": profile}
@app.get("/api/auth/challenge")
async def auth_challenge():
return {"ok": True, **_create_challenge()}
@app.post("/api/auth/register")
async def auth_register(req: AuthCredentialRequest, response: Response):
async def auth_register(req: AuthCredentialRequest, request: Request, response: Response):
ip = _client_ip(request)
if _hit_limit(_register_rate, f"ip:{ip}", limit=8, window_sec=600):
return {"ok": False, "detail": "请求过于频繁,请稍后再试"}
if _hit_limit(_register_rate, f"user:{(req.username or '').strip().lower()}", limit=6, window_sec=600):
return {"ok": False, "detail": "该用户名操作过于频繁,请稍后再试"}
username = (req.username or "").strip()
password = req.password or ""
if len(username) < 2:
return {"ok": False, "detail": "用户名至少 2 个字符"}
if len(password) < 6:
return {"ok": False, "detail": "密码至少 6 个字符"}
ok, msg = _validate_username_password(username, password)
if not ok:
return {"ok": False, "detail": msg}
ok_challenge, challenge_msg = _verify_challenge(req)
if not ok_challenge:
return {"ok": False, "detail": challenge_msg}
try:
user = users.create_user(username, password)
except Exception as exc:
@@ -225,14 +481,19 @@ async def auth_register(req: AuthCredentialRequest, response: Response):
)
return {
"ok": True,
"detail": "注册并登录成功,已赠送试用 token,请保存重置码",
"detail": "注册并登录成功,已赠送试用 Credits,请保存重置码",
"user": {"id": user["id"], "username": user["username"]},
"reset_code": user.get("reset_code", ""),
}
@app.post("/api/auth/login")
async def auth_login(req: AuthCredentialRequest, response: Response):
async def auth_login(req: AuthCredentialRequest, request: Request, response: Response):
ip = _client_ip(request)
if _hit_limit(_login_rate, f"ip:{ip}", limit=20, window_sec=600):
return {"ok": False, "detail": "登录过于频繁,请稍后再试"}
if _hit_limit(_login_rate, f"user:{(req.username or '').strip().lower()}", limit=12, window_sec=600):
return {"ok": False, "detail": "该账户登录尝试过多,请稍后再试"}
try:
user = users.verify_user((req.username or "").strip(), req.password or "")
except Exception as exc:
@@ -335,6 +596,217 @@ async def auth_delete_account(req: DeleteAccountRequest, request: Request, respo
return {"ok": True, "detail": "账号已注销,关联数据已清空"}
@app.get("/api/auth/vip/status")
async def auth_vip_status(request: Request):
user = _require_user(request)
if not user:
return {"ok": False, "detail": "请先登录"}
return {"ok": True, "vip": users.get_vip_status(user["id"])}
@app.post("/api/auth/vip/toggle")
async def auth_vip_toggle(req: VipToggleRequest, request: Request):
user = _require_user(request)
if not user:
return {"ok": False, "detail": "请先登录"}
vip = users.set_vip_enabled(user["id"], bool(req.enabled))
return {"ok": True, "detail": "VIP 模式已更新", "vip": vip}
@app.post("/api/auth/vip/recharge")
async def auth_vip_recharge(req: VipRechargeRequest, request: Request):
token = (request.headers.get("X-Shop-Token") or "").strip()
if not settings.shop_backend_callback_token or token != settings.shop_backend_callback_token:
raise HTTPException(status_code=403, detail="直充接口已禁用,请走支付订单接口")
user = _require_user(request)
if not user:
return {"ok": False, "detail": "请先登录"}
vip = users.recharge_tokens(
user["id"],
int(req.tokens),
kind="admin_direct_recharge",
ref_type="admin",
ref_id="",
detail={"source": "legacy_api"},
)
return {"ok": True, "detail": "充值成功", "vip": vip}
@app.get("/api/billing/overview")
async def billing_overview(request: Request):
user = _require_user(request)
if not user:
return {"ok": False, "detail": "请先登录"}
return {
"ok": True,
"vip": users.get_vip_status(user["id"]),
"recharge_records": users.list_recharge_orders(user["id"], limit=30),
"consume_records": users.list_token_ledger(user["id"], limit=100),
}
@app.post("/api/billing/recharge/create")
async def billing_recharge_create(req: BillingRechargeCreateRequest, request: Request):
user = _require_user(request)
if not user:
return {"ok": False, "detail": "请先登录"}
order, pay_url = await _create_recharge_order_with_pay_url(
user_id=user["id"],
username=user["username"],
credits=int(req.tokens),
callback_url=f"{str(request.base_url).rstrip('/')}/api/pay/wechat/backcall",
order_meta={
"subscriber_name": (req.subscriber_name or "").strip(),
"subscriber_phone": (req.subscriber_phone or "").strip(),
"shipping_address": (req.shipping_address or "").strip(),
},
channel=(req.channel or "wechat").strip() or "wechat",
)
return {"ok": True, "detail": "充值订单已创建", "order": order, "pay_url": pay_url}
@app.post("/api/billing/recharge/pay-now")
async def billing_recharge_pay_now(req: BillingPayNowRequest, request: Request):
user = _require_user(request)
if not user:
return {"ok": False, "detail": "请先登录"}
order_no = (req.order_no or "").strip()
if not order_no:
return {"ok": False, "detail": "订单号不能为空"}
order = users.get_recharge_order(user["id"], order_no)
if not order:
return {"ok": False, "detail": "订单不存在"}
status = (order.get("status") or "").lower()
if status in {"paid", "success"}:
return {"ok": False, "detail": "订单已支付,无需重复支付", "order": order}
if status in {"cancelled", "closed"}:
return {"ok": False, "detail": "订单已取消,请重新创建订单", "order": order}
pay_url = await _fetch_pay_url_for_order(
order=order,
user_id=user["id"],
username=user["username"],
callback_url=f"{str(request.base_url).rstrip('/')}/api/pay/wechat/backcall",
)
return {"ok": True, "detail": "支付链接已生成", "order": order, "pay_url": pay_url}
@app.get("/api/pay/wechat/")
async def pay_wechat_ready():
return {"ok": True, "detail": "wechat pay api ready"}
@app.post("/api/pay/wechat/")
async def pay_wechat_create(req: BillingRechargeCreateRequest, request: Request):
user = _require_user(request)
if not user:
return {"ok": False, "detail": "请先登录"}
order, pay_url = await _create_recharge_order_with_pay_url(
user_id=user["id"],
username=user["username"],
credits=int(req.tokens),
callback_url=f"{str(request.base_url).rstrip('/')}/api/pay/wechat/backcall",
order_meta={
"subscriber_name": (req.subscriber_name or "").strip(),
"subscriber_phone": (req.subscriber_phone or "").strip(),
"shipping_address": (req.shipping_address or "").strip(),
},
channel=(req.channel or "wechat").strip() or "wechat",
)
return {"ok": True, "detail": "支付订单已创建", "order": order, "pay_url": pay_url}
@app.get("/api/pay/address/suggest")
async def pay_address_suggest(request: Request):
ip = _client_ip(request)
if not ip or ip in {"unknown", "127.0.0.1", "::1"}:
return {"ok": True, "detail": "本地环境,建议手动填写地址", "ip": ip or "unknown", "address": ""}
url = f"https://ip-api.com/json/{ip}?lang=zh-CN"
try:
async with httpx.AsyncClient(timeout=5) as client:
r = await client.get(url)
body = r.json() if r.content else {}
if r.status_code >= 400 or not isinstance(body, dict):
return {"ok": True, "detail": "IP地址解析失败已填入IP信息可手动修改", "ip": ip, "address": f"IP:{ip}"}
if body.get("status") != "success":
return {"ok": True, "detail": "IP地址解析失败已填入IP信息可手动修改", "ip": ip, "address": f"IP:{ip}"}
country = str(body.get("country") or "").strip()
region = str(body.get("regionName") or "").strip()
city = str(body.get("city") or "").strip()
isp = str(body.get("isp") or "").strip()
addr = "".join([x for x in [country, region, city] if x])
if isp:
addr = f"{addr}{isp}" if addr else isp
if not addr:
addr = f"IP:{ip}"
return {"ok": True, "detail": "已根据IP自动识别地址并填充可手动修改", "ip": ip, "address": addr}
except Exception:
return {"ok": True, "detail": "IP地址解析失败已填入IP信息可手动修改", "ip": ip, "address": f"IP:{ip}"}
@app.post("/api/billing/recharge/notify")
async def billing_recharge_notify(req: BillingRechargeNotifyRequest, request: Request):
token = (request.headers.get("X-Shop-Token") or "").strip()
if not settings.shop_backend_callback_token or token != settings.shop_backend_callback_token:
raise HTTPException(status_code=403, detail="forbidden")
user = _require_user(request)
uid = user["id"] if user else 0
# 回调优先根据登录态用户确认;若无登录态,可通过订单所属用户在 DB 内判断
if uid <= 0:
# 无用户态时,先查订单归属(通过 mark 接口会二次校验)
uid = -1
if (req.status or "").lower() not in {"paid", "success"}:
return {"ok": True, "detail": "ignored"}
if uid > 0:
ok, msg = users.mark_recharge_order_paid(
uid,
req.order_no,
paid_amount_cny=float(req.paid_amount_cny or 0.0),
external_txn_id=(req.external_txn_id or "").strip(),
meta={"status": req.status},
)
if not ok:
return {"ok": False, "detail": msg}
return {"ok": True, "detail": msg}
# no session callback path: iterate known users is unavailable; use direct sql in store by probing order owner
# fallback: let store resolve user by order internally via new helper-like behavior
owner_id = users.get_recharge_order_user_id(req.order_no)
if not owner_id:
return {"ok": False, "detail": "订单不存在"}
ok, msg = users.mark_recharge_order_paid(
int(owner_id),
req.order_no,
paid_amount_cny=float(req.paid_amount_cny or 0.0),
external_txn_id=(req.external_txn_id or "").strip(),
meta={"status": req.status},
)
if not ok:
return {"ok": False, "detail": msg}
return {"ok": True, "detail": msg}
@app.post("/api/pay/wechat/backcall")
async def pay_wechat_backcall(req: BillingRechargeNotifyRequest, request: Request):
configured = (settings.shop_backend_callback_token or "").strip()
provided = (request.headers.get("X-Shop-Token") or request.query_params.get("token") or "").strip()
if configured and provided != configured:
raise HTTPException(status_code=403, detail="forbidden")
if (req.status or "").lower() not in {"paid", "success"}:
return {"ok": True, "detail": "ignored"}
owner_id = users.get_recharge_order_user_id(req.order_no)
if not owner_id:
return {"ok": False, "detail": "订单不存在"}
ok, msg = users.mark_recharge_order_paid(
int(owner_id),
req.order_no,
paid_amount_cny=float(req.paid_amount_cny or 0.0),
external_txn_id=(req.external_txn_id or "").strip(),
meta={"status": req.status, "source": "wechat_backcall"},
)
if not ok:
return {"ok": False, "detail": msg}
return {"ok": True, "detail": msg}
@app.post("/api/auth/wechat/bind")
async def auth_wechat_bind(req: WechatBindingRequest, request: Request):
user = _require_user(request)
@@ -425,6 +897,20 @@ async def auth_ai_model_delete(req: AIModelDeleteRequest, request: Request):
return {"ok": True, "detail": "模型配置已删除"}
@app.post("/api/auth/ai-models/image-model/update")
async def auth_ai_image_model_update(req: AIImageModelUpdateRequest, request: Request):
user = _require_user(request)
if not user:
return {"ok": False, "detail": "请先登录"}
image_model = (req.image_model or "").strip()
if not image_model:
return {"ok": False, "detail": "文生图模型不能为空"}
ok = users.update_active_ai_image_model(user["id"], image_model)
if not ok:
return {"ok": False, "detail": "保存失败:请先配置文本模型"}
return {"ok": True, "detail": "文生图模型已更新"}
@app.post("/api/rewrite")
async def rewrite(req: RewriteRequest, request: Request):
rid = getattr(request.state, "request_id", "")
@@ -444,7 +930,9 @@ async def rewrite(req: RewriteRequest, request: Request):
user = _require_user(request)
if not user:
raise HTTPException(status_code=401, detail="请先登录")
model_cfg = users.get_active_ai_model(user["id"])
model_cfg, model_source = _select_model_cfg(user["id"], prefer_vip=True)
if model_source == "vip_empty":
raise HTTPException(status_code=402, detail=_quota_detail())
if not model_cfg:
raise HTTPException(status_code=400, detail="请先在设置页配置 AI 模型")
backup = {
@@ -472,6 +960,50 @@ async def rewrite(req: RewriteRequest, request: Request):
settings.openai_max_output_tokens = backup["openai_max_output_tokens"]
settings.openai_max_retries = backup["openai_max_retries"]
settings.openai_image_model = backup["openai_image_model"]
usage = ((result.trace or {}).get("usage") or {}) if isinstance(result.trace, dict) else {}
prompt_tokens = int(usage.get("prompt_tokens") or 0)
completion_tokens = int(usage.get("completion_tokens") or 0)
total_tokens = int(usage.get("total_tokens") or 0)
billed_basis = "usage_tokens" if total_tokens > 0 else "char_estimate"
token_cost = _estimate_rewrite_cost(req, result)
vip_status = users.get_vip_status(user["id"])
should_consume = bool(vip_status.get("vip_enabled"))
if should_consume:
ok_cost, balance = users.consume_tokens(
user["id"],
token_cost,
kind="rewrite",
ref_type="request",
ref_id=rid,
detail={
"source_chars": len((req.source_text or "").strip()),
"target_body_chars": int(req.target_body_chars or 0),
"title_chars": len((result.title or "").strip()),
"summary_chars": len((result.summary or "").strip()),
"body_chars": len((result.body_markdown or "").strip()),
"model": model_cfg.get("model") if model_cfg else "",
"prompt_tokens": prompt_tokens,
"completion_tokens": completion_tokens,
"total_tokens": total_tokens,
"credits_rule": (
f"1000000 tokens={float(settings.credits_token_price_per_million_cny):.2f}元,"
f"图片{int(settings.credits_image_price_package_images)}张={float(settings.credits_image_price_package_cny):.2f}元,"
f"{int(settings.credits_recharge_package_credits)}Credits={float(settings.credits_recharge_package_amount):.2f}"
),
"billed_basis": billed_basis,
},
)
if not ok_cost:
raise HTTPException(status_code=402, detail=_quota_detail())
if result.trace is None:
result.trace = {}
result.trace["vip"] = {
"model_source": "platform" if model_source == "vip" else "user",
"credits_cost": token_cost,
"credits_balance": balance,
"billed_basis": billed_basis,
"total_tokens": total_tokens,
}
tr = result.trace or {}
logger.info(
"api_rewrite_out rid=%s mode=%s duration_ms=%s quality_notes=%d trace_steps=%s soft_accept=%s",
@@ -599,7 +1131,10 @@ async def generate_wechat_cover(req: WechatCoverGenerateRequest, request: Reques
len(req.summary or ""),
req.upload_to_wechat,
)
model_cfg = users.get_active_ai_model(user["id"])
model_cfg, model_source = _select_model_cfg(user["id"], prefer_vip=True)
if model_source == "vip_empty":
return {"ok": False, "detail": _quota_detail(), "upgrade_required": True}
image_model_override = (req.image_model or "").strip()
backup = {
"openai_api_key": settings.openai_api_key,
"openai_base_url": settings.openai_base_url,
@@ -626,6 +1161,8 @@ async def generate_wechat_cover(req: WechatCoverGenerateRequest, request: Reques
settings.openai_max_output_tokens = 8192
settings.openai_max_retries = 0
settings.openai_image_model = backup["openai_image_model"]
if image_model_override:
settings.openai_image_model = image_model_override
out = await PosterMaterialService(wechat).generate_cover(req, request_id=rid, account=binding)
finally:
settings.openai_api_key = backup["openai_api_key"]
@@ -643,6 +1180,31 @@ async def generate_wechat_cover(req: WechatCoverGenerateRequest, request: Reques
out.note,
len(out.warnings),
)
if out.ok and model_source == "vip":
token_cost = _estimate_image_cost(1)
ok_cost, balance = users.consume_tokens(
user["id"],
token_cost,
kind="cover_generate",
ref_type="request",
ref_id=rid,
detail={
"title": (req.title or "")[:120],
"style_hint": (req.style_hint or "")[:120],
"image_count": 1,
"image_model": image_model_override or settings.openai_image_model,
"image_price_package_cny": float(settings.credits_image_price_package_cny),
"image_price_package_images": int(settings.credits_image_price_package_images),
"credits_rule": (
f"图片{int(settings.credits_image_price_package_images)}张={float(settings.credits_image_price_package_cny):.2f}元,"
f"{int(settings.credits_recharge_package_credits)}Credits={float(settings.credits_recharge_package_amount):.2f}"
),
},
)
if not ok_cost:
return {"ok": False, "detail": _quota_detail(), "upgrade_required": True}
out.warnings = list(out.warnings or [])
out.warnings.append(f"已扣减 {token_cost} Credits可用余额 {balance}")
return out
@@ -684,7 +1246,10 @@ async def generate_posters(req: PosterGenerateRequest, request: Request):
req.upload_to_wechat,
int(req.max_images or 0),
)
model_cfg = users.get_active_ai_model(user["id"])
model_cfg, model_source = _select_model_cfg(user["id"], prefer_vip=True)
if model_source == "vip_empty":
return {"ok": False, "detail": _quota_detail(), "upgrade_required": True}
image_model_override = (req.image_model or "").strip()
backup = {
"openai_api_key": settings.openai_api_key,
"openai_base_url": settings.openai_base_url,
@@ -711,6 +1276,8 @@ async def generate_posters(req: PosterGenerateRequest, request: Request):
settings.openai_max_output_tokens = 8192
settings.openai_max_retries = 0
settings.openai_image_model = backup["openai_image_model"]
if image_model_override:
settings.openai_image_model = image_model_override
out = await PosterMaterialService(wechat).generate(req, request_id=rid, account=binding)
finally:
settings.openai_api_key = backup["openai_api_key"]
@@ -727,6 +1294,32 @@ async def generate_posters(req: PosterGenerateRequest, request: Request):
len(out.posters),
len(out.warnings),
)
if out.ok and model_source == "vip":
image_count = max(0, len(out.posters or []))
token_cost = _estimate_image_cost(image_count)
ok_cost, balance = users.consume_tokens(
user["id"],
token_cost,
kind="poster_generate",
ref_type="request",
ref_id=rid,
detail={
"image_count": image_count,
"body_chars": len((req.body_markdown or "").strip()),
"max_images": int(req.max_images or 0),
"image_model": image_model_override or settings.openai_image_model,
"image_price_package_cny": float(settings.credits_image_price_package_cny),
"image_price_package_images": int(settings.credits_image_price_package_images),
"credits_rule": (
f"图片{int(settings.credits_image_price_package_images)}张={float(settings.credits_image_price_package_cny):.2f}元,"
f"{int(settings.credits_recharge_package_credits)}Credits={float(settings.credits_recharge_package_amount):.2f}"
),
},
)
if not ok_cost:
return {"ok": False, "detail": _quota_detail(), "upgrade_required": True}
out.warnings = list(out.warnings or [])
out.warnings.append(f"已扣减 {token_cost} Credits可用余额 {balance}")
return out