Unverified Commit 2559b3e7 authored by LiangLiu's avatar LiangLiu Committed by GitHub
Browse files

Update user token auto refresh (#477)


Co-authored-by: default avatarqinxinyi <qxy118045534@163.com>
parent 63f0486f
......@@ -46,6 +46,10 @@ class TTSRequest(BaseModel):
resource_id: str = "seed-tts-1.0"
class RefreshTokenRequest(BaseModel):
refresh_token: str
# =========================
# FastAPI Related Code
# =========================
......@@ -147,6 +151,18 @@ def error_response(e, code):
return JSONResponse({"message": f"error: {e}!"}, status_code=code)
def format_user_response(user):
return {
"user_id": user.get("user_id"),
"id": user.get("id"),
"source": user.get("source"),
"username": user.get("username") or "",
"email": user.get("email") or "",
"homepage": user.get("homepage") or "",
"avatar_url": user.get("avatar_url") or "",
}
def guess_file_type(name, default_type):
content_type, _ = mimetypes.guess_type(name)
if content_type is None:
......@@ -183,9 +199,10 @@ async def github_callback(request: Request):
user_info = await auth_manager.auth_github(code)
user_id = await task_manager.create_user(user_info)
user_info["user_id"] = user_id
access_token = auth_manager.create_jwt_token(user_info)
logger.info(f"GitHub callback: user_info: {user_info}, access_token: {access_token}")
return {"access_token": access_token, "user_info": user_info}
user_response = format_user_response(user_info)
access_token, refresh_token = auth_manager.create_tokens(user_response)
logger.info(f"GitHub callback: user_info: {user_response}, access token issued")
return {"access_token": access_token, "refresh_token": refresh_token, "user_info": user_response}
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
......@@ -209,9 +226,10 @@ async def google_callback(request: Request):
user_info = await auth_manager.auth_google(code)
user_id = await task_manager.create_user(user_info)
user_info["user_id"] = user_id
access_token = auth_manager.create_jwt_token(user_info)
logger.info(f"Google callback: user_info: {user_info}, access_token: {access_token}")
return {"access_token": access_token, "user_info": user_info}
user_response = format_user_response(user_info)
access_token, refresh_token = auth_manager.create_tokens(user_response)
logger.info(f"Google callback: user_info: {user_response}, access token issued")
return {"access_token": access_token, "refresh_token": refresh_token, "user_info": user_response}
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
......@@ -245,10 +263,31 @@ async def sms_callback(request: Request):
user_id = await task_manager.create_user(user_info)
user_info["user_id"] = user_id
access_token = auth_manager.create_jwt_token(user_info)
logger.info(f"SMS callback: user_info: {user_info}, access_token: {access_token}")
return {"access_token": access_token, "user_info": user_info}
user_response = format_user_response(user_info)
access_token, refresh_token = auth_manager.create_tokens(user_response)
logger.info(f"SMS callback: user_info: {user_response}, access token issued")
return {"access_token": access_token, "refresh_token": refresh_token, "user_info": user_response}
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.post("/auth/refresh")
async def refresh_access_token(request: RefreshTokenRequest):
try:
payload = auth_manager.verify_refresh_token(request.refresh_token)
user_id = payload.get("user_id")
if not user_id:
raise HTTPException(status_code=401, detail="Invalid refresh token")
user = await task_manager.query_user(user_id)
if user is None or user.get("user_id") != user_id:
raise HTTPException(status_code=401, detail="Invalid user")
user_info = format_user_response(user)
access_token, refresh_token = auth_manager.create_tokens(user_info)
return {"access_token": access_token, "refresh_token": refresh_token, "user_info": user_info}
except HTTPException as exc:
raise exc
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
......
import os
import time
import uuid
import aiohttp
import jwt
......@@ -24,8 +25,10 @@ class AuthManager:
self.google_redirect_uri = os.getenv("GOOGLE_REDIRECT_URI", "")
self.jwt_algorithm = os.getenv("JWT_ALGORITHM", "HS256")
self.jwt_expiration_hours = os.getenv("JWT_EXPIRATION_HOURS", 24)
self.jwt_secret_key = os.getenv("JWT_SECRET_KEY", "your-secret-key-change-in-production")
self.jwt_expiration_hours = int(os.getenv("JWT_EXPIRATION_HOURS", "168"))
self.refresh_token_expiration_days = int(os.getenv("REFRESH_TOKEN_EXPIRATION_DAYS", "30"))
self.refresh_jwt_secret_key = os.getenv("REFRESH_JWT_SECRET_KEY", self.jwt_secret_key)
# Aliyun SMS
self.aliyun_client = AlibabaCloudClient()
......@@ -38,16 +41,32 @@ class AuthManager:
logger.info(f"AuthManager: JWT_SECRET_KEY: {self.jwt_secret_key}")
logger.info(f"AuthManager: WORKER_SECRET_KEY: {self.worker_secret_key}")
def create_jwt_token(self, data):
data2 = {
def _create_token(self, data, expires_in_seconds, token_type, secret_key):
now = int(time.time())
payload = {
"user_id": data["user_id"],
"username": data["username"],
"email": data["email"],
"homepage": data["homepage"],
"token_type": token_type,
"iat": now,
"exp": now + expires_in_seconds,
"jti": str(uuid.uuid4()),
}
expire = time.time() + (self.jwt_expiration_hours * 3600)
data2.update({"exp": expire})
return jwt.encode(data2, self.jwt_secret_key, algorithm=self.jwt_algorithm)
return jwt.encode(payload, secret_key, algorithm=self.jwt_algorithm)
def create_access_token(self, data):
return self._create_token(data, self.jwt_expiration_hours * 3600, "access", self.jwt_secret_key)
def create_refresh_token(self, data):
return self._create_token(data, self.refresh_token_expiration_days * 24 * 3600, "refresh", self.refresh_jwt_secret_key)
def create_tokens(self, data):
return self.create_access_token(data), self.create_refresh_token(data)
def create_jwt_token(self, data):
# Backwards compatibility for callers that still expect this name
return self.create_access_token(data)
async def auth_github(self, code):
try:
......@@ -163,9 +182,12 @@ class AuthManager:
"avatar_url": "",
}
def verify_jwt_token(self, token):
def _verify_token(self, token, expected_type, secret_key):
try:
payload = jwt.decode(token, self.jwt_secret_key, algorithms=[self.jwt_algorithm])
payload = jwt.decode(token, secret_key, algorithms=[self.jwt_algorithm])
token_type = payload.get("token_type")
if token_type != expected_type:
raise HTTPException(status_code=401, detail="Token type mismatch")
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token has expired")
......@@ -173,5 +195,11 @@ class AuthManager:
logger.error(f"verify_jwt_token error: {e}")
raise HTTPException(status_code=401, detail="Could not validate credentials")
def verify_jwt_token(self, token):
return self._verify_token(token, "access", self.jwt_secret_key)
def verify_refresh_token(self, token):
return self._verify_token(token, "refresh", self.refresh_jwt_secret_key)
def verify_worker_token(self, token):
return token == self.worker_secret_key
......@@ -13,21 +13,30 @@
<meta name="theme-color" content="#0f1329">
<link rel="canonical" href="https://x2v.light-ai.top/" />
<link rel="alternate" hreflang="zh-CN" href="https://x2v.light-ai.top/" />
<link rel="alternate" hreflang="en" href="https://x2v.light-ai.top/en/" />
<link rel="alternate" hreflang="en" href="https://x2v.light-ai.top/" />
<link rel="alternate" hreflang="x-default" href="https://x2v.light-ai.top/" />
<!-- Open Graph for social platforms -->
<meta property="og:type" content="website">
<meta property="og:site_name" content="LightX2V">
<meta property="og:title" content="LightX2V — AI数字人视频生成平台">
<meta property="og:description" content="轻量、快速的AI数字人视频生成平台。在线体验、示例视频与使用说明。">
<meta property="og:url" content="https://x2v.light-ai.top/">
<meta property="og:image" content="https://x2v.light-ai.top/og-cover.jpg">
<meta property="og:image" content="/public/cover.png">
<!-- Twitter Card (for X/Twitter) -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="LightX2V — AI数字人视频生成平台">
<meta name="twitter:description" content="轻量、快速的AI数字人视频生成平台。由 Light AI 工具链驱动,支持文本/图像到视频的高效生成。">
<meta name="twitter:image" content="https://x2v.light-ai.top/og-cover.jpg">
<link rel="icon" href="/favicon.ico" sizes="32x32">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<meta name="twitter:image" content="/public/cover.png">
<meta name="twitter:site" content="@LightX2V">
<!-- Favicon & App icons -->
<link rel="icon" href="/public/logo_black.png" sizes="32x32">
<link rel="icon" href="/public/logo_black.png" type="image/png">
<link rel="icon" href="/public/logo_black.png" type="image/x-icon">
<link rel="apple-touch-icon" sizes="180x180" href="/public/logo_black.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="preconnect" href="https://x2v.light-ai.top" crossorigin>
<link rel="preconnect" href="https://cdn.bootcdn.net" crossorigin>
......
<svg width="765" height="669" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" overflow="hidden"><g transform="translate(-2530 -605)"><g><path d="M3022.72 606.11C3025.74 606.113 3025.74 606.113 3028.82 606.117 3031.12 606.114 3033.42 606.11 3035.79 606.107 3038.33 606.116 3040.88 606.124 3043.5 606.133 3047.5 606.133 3047.5 606.133 3051.57 606.132 3060.42 606.134 3069.27 606.153 3078.12 606.172 3084.24 606.176 3090.36 606.18 3096.48 606.182 3110.96 606.19 3125.45 606.21 3139.94 606.234 3156.43 606.261 3172.92 606.274 3189.41 606.286 3223.34 606.311 3257.26 606.354 3291.19 606.408 3287.04 615.065 3282.01 622.098 3276.09 629.687 3275.04 631.034 3274 632.382 3272.92 633.769 3270.6 636.746 3268.29 639.72 3265.97 642.692 3262 647.781 3258.05 652.885 3254.1 657.992 3239.44 676.934 3224.67 695.764 3209.67 714.436 3201.04 725.206 3192.58 736.099 3184.2 747.068 3176.85 756.678 3169.41 766.208 3161.93 775.711 3152.96 787.105 3144.04 798.535 3135.2 810.024 3133.25 812.553 3131.3 815.081 3129.3 817.687 3125.94 822.137 3122.73 826.7 3119.64 831.339 3174.66 831.339 3229.68 831.339 3286.36 831.339 3282.06 839.942 3279.32 843.707 3272.62 850.083 3264.57 857.934 3257.08 866.031 3249.75 874.552 3244.71 880.373 3239.57 886.09 3234.41 891.804 3227.91 899.013 3221.45 906.249 3215.08 913.571 3206.88 922.992 3198.53 932.262 3190.16 941.536 3185.54 946.692 3180.97 951.883 3176.42 957.106 3168.23 966.527 3159.87 975.797 3151.51 985.072 3146.88 990.227 3142.31 995.418 3137.76 1000.64 3131.39 1007.96 3124.93 1015.2 3118.43 1022.41 3106.79 1035.34 3095.29 1048.39 3084 1061.65 3070.53 1077.46 3056.92 1093.08 3042.88 1108.4 3038.2 1113.54 3033.59 1118.74 3029.04 1123.99 3022.3 1131.75 3015.42 1139.37 3008.5 1146.97 2996.53 1160.12 2984.76 1173.39 2973.29 1186.98 2958.33 1204.68 2942.93 1221.84 2926.96 1238.65 2917.56 1248.55 2908.57 1258.67 2899.77 1269.11 2897.35 1264.27 2897.35 1264.27 2899.39 1258.04 2901.03 1254.06 2901.03 1254.06 2902.71 1250.01 2903.32 1248.55 2903.92 1247.08 2904.54 1245.57 2906.56 1240.66 2908.6 1235.76 2910.64 1230.86 2912.07 1227.41 2913.49 1223.96 2914.91 1220.51 2917.15 1215.04 2919.41 1209.58 2921.66 1204.11 2929.65 1184.77 2937.48 1165.36 2945.31 1145.95 2978.1 1064.69 2978.1 1064.69 2990.83 1037.37 2996.42 1025.28 3000.81 1012.88 3005.1 1000.27 3009.83 987.329 3015.28 974.664 3020.58 961.943 2971.94 961.943 2923.3 961.943 2873.19 961.943 2878.03 947.4 2883.01 933.108 2888.59 918.862 2889.27 917.127 2889.94 915.392 2890.63 913.604 2896.65 898.172 2903.03 882.896 2909.43 867.618 2928.93 821.008 2948.27 774.341 2967.17 727.485 2976.34 704.764 2985.57 682.079 2995.17 659.533 3001.38 644.917 3007.24 630.174 3013.02 615.383 3016.71 606.502 3016.71 606.502 3022.72 606.11Z" fill="#000000" fill-rule="evenodd" fill-opacity="1"/><path d="M2706.2 612.344C2709.12 612.342 2712.05 612.34 2715.06 612.338 2723.07 612.346 2731.07 612.407 2739.07 612.492 2747.44 612.569 2755.81 612.576 2764.19 612.59 2780.03 612.628 2795.87 612.729 2811.71 612.852 2829.76 612.989 2847.8 613.056 2865.84 613.117 2902.94 613.245 2940.04 613.463 2977.14 613.733 2972.36 626.655 2967.57 639.578 2962.64 652.891 2925.96 653.118 2889.28 653.293 2852.6 653.399 2835.57 653.449 2818.54 653.518 2801.5 653.63 2785.07 653.738 2768.64 653.795 2752.21 653.82 2745.94 653.838 2739.66 653.874 2733.39 653.926 2724.61 653.998 2715.84 654.008 2707.06 654.003 2704.46 654.038 2701.86 654.073 2699.17 654.109 2681.63 654.011 2681.63 654.011 2672.13 648.453 2665.75 640.869 2664.86 635.872 2665.21 625.97 2675.56 609.983 2689.1 612.072 2706.2 612.344Z" fill="#000000" fill-rule="evenodd" fill-opacity="1"/><path d="M2567.49 765.244C2572.21 765.187 2572.21 765.187 2577.03 765.129 2580.45 765.151 2583.86 765.175 2587.28 765.203 2590.8 765.195 2594.33 765.183 2597.85 765.168 2605.23 765.15 2612.6 765.176 2619.97 765.232 2629.41 765.3 2638.84 765.261 2648.28 765.189 2655.55 765.146 2662.82 765.16 2670.1 765.191 2673.58 765.199 2677.05 765.189 2680.53 765.161 2685.4 765.131 2690.27 765.185 2695.13 765.244 2697.9 765.254 2700.66 765.263 2703.51 765.273 2713.25 767.275 2716.93 771.094 2722.99 778.796 2723.29 787.346 2723.29 787.346 2720.58 795.595 2711.61 804.572 2705.51 806.411 2692.94 806.513 2689.84 806.559 2686.74 806.605 2683.55 806.652 2680.18 806.651 2676.82 806.646 2673.46 806.639 2669.99 806.657 2666.52 806.678 2663.05 806.701 2655.79 806.737 2648.53 806.735 2641.27 806.71 2631.98 806.682 2622.69 806.764 2613.4 806.875 2606.24 806.946 2599.08 806.95 2591.92 806.935 2588.49 806.937 2585.07 806.962 2581.64 807.011 2555 807.341 2555 807.341 2546.9 802.088 2542.19 796.699 2539.82 791.552 2538.12 784.646 2542.01 769.555 2553.07 765.294 2567.49 765.244Z" fill="#000000" fill-rule="evenodd" fill-opacity="1"/><path d="M2712.13 920.161C2715.21 920.161 2718.3 920.162 2721.48 920.162 2724.84 920.199 2728.2 920.237 2731.56 920.275 2735 920.289 2738.45 920.299 2741.89 920.306 2750.95 920.333 2760 920.404 2769.05 920.484 2778.29 920.558 2787.53 920.59 2796.78 920.627 2814.9 920.704 2833.02 920.827 2851.14 920.978 2847.13 932.985 2843.11 944.991 2839.09 956.998 2820.72 957.22 2802.34 957.391 2783.96 957.495 2775.43 957.545 2766.89 957.614 2758.36 957.722 2748.55 957.847 2738.74 957.893 2728.92 957.936 2725.86 957.985 2722.8 958.035 2719.65 958.086 2715.38 958.087 2715.38 958.087 2711.02 958.088 2708.52 958.109 2706.01 958.131 2703.43 958.153 2695.18 956.7 2692.37 953.84 2687.23 947.392 2685.37 940.287 2686.27 935.705 2687.23 928.182 2694.83 920.197 2701.41 920.098 2712.13 920.161Z" fill="#000000" fill-rule="evenodd" fill-opacity="1"/><path d="M2805.67 765.653C2808.76 765.671 2811.86 765.688 2815.06 765.705 2823.27 765.751 2831.47 765.87 2839.68 766.004 2848.07 766.128 2856.46 766.183 2864.85 766.243 2881.29 766.376 2897.72 766.579 2914.16 766.833 2913.69 768.207 2913.22 769.58 2912.74 770.995 2908.88 782.425 2905.28 793.862 2902.06 805.488 2884.01 805.856 2865.96 806.109 2847.91 806.284 2841.77 806.357 2835.63 806.456 2829.5 806.581 2820.67 806.757 2811.85 806.84 2803.02 806.903 2800.27 806.978 2797.53 807.053 2794.7 807.13 2776.14 807.138 2776.14 807.138 2769.33 802.382 2764.56 796.899 2761.88 791.841 2760.16 784.801 2761.85 778.235 2764.24 774.087 2769.17 769.396 2779.97 763.388 2793.68 765.405 2805.67 765.653Z" fill="#000000" fill-rule="evenodd" fill-opacity="1"/><path d="M2692.53 997.251C2695.22 997.245 2697.92 997.239 2700.7 997.233 2706.38 997.235 2712.07 997.268 2717.76 997.328 2726.47 997.412 2735.18 997.378 2743.9 997.331 2749.43 997.348 2754.95 997.371 2760.48 997.402 2763.09 997.39 2765.7 997.378 2768.39 997.366 2772.03 997.424 2772.03 997.424 2775.74 997.482 2778.94 997.505 2778.94 997.505 2782.2 997.528 2789.81 999.175 2792.75 1002.52 2797.62 1008.47 2798.99 1013.31 2798.99 1013.31 2798.98 1018.16 2799.03 1019.76 2799.09 1021.36 2799.14 1023.01 2796.62 1031.06 2792.73 1033.19 2785.54 1037.54 2776.49 1039.03 2767.52 1038.96 2758.36 1038.92 2755.72 1038.93 2753.09 1038.95 2750.37 1038.96 2744.81 1038.98 2739.24 1038.97 2733.68 1038.94 2725.17 1038.91 2716.66 1038.99 2708.15 1039.08 2702.74 1039.09 2697.33 1039.08 2691.92 1039.07 2689.37 1039.1 2686.83 1039.13 2684.21 1039.17 2666.61 1038.94 2666.61 1038.94 2657.09 1031.85 2652.65 1025.43 2652.65 1025.43 2651.14 1018.16 2655.78 995.844 2673.84 997.02 2692.53 997.251Z" fill="#000000" fill-rule="evenodd" fill-opacity="1"/><path d="M2876.55 997.375C2879.23 997.392 2881.9 997.409 2884.66 997.427 2893.19 997.495 2901.72 997.648 2910.25 997.802 2916.05 997.863 2921.84 997.919 2927.63 997.969 2941.81 998.103 2955.99 998.307 2970.17 998.563 2965.93 1011.87 2961.29 1024.75 2955.61 1037.51 2940.62 1037.87 2925.64 1038.14 2910.65 1038.31 2905.55 1038.39 2900.46 1038.49 2895.36 1038.61 2888.03 1038.79 2880.7 1038.87 2873.37 1038.94 2871.09 1039.01 2868.82 1039.09 2866.48 1039.17 2851.03 1039.17 2851.03 1039.17 2844.66 1034.38 2839.85 1028.81 2836.91 1023.84 2835.17 1016.67 2840.62 995.348 2858.58 996.939 2876.55 997.375Z" fill="#000000" fill-rule="evenodd" fill-opacity="1"/><path d="M2791.15 842.436C2795.09 842.456 2795.09 842.456 2799.12 842.477 2807.5 842.531 2815.89 842.653 2824.27 842.776 2829.96 842.824 2835.65 842.868 2841.35 842.908 2855.28 843.014 2869.22 843.176 2883.16 843.38 2878.9 856.593 2874.25 869.379 2868.55 882.047 2853.47 882.286 2838.4 882.461 2823.32 882.578 2818.19 882.627 2813.06 882.693 2807.93 882.776 2800.56 882.893 2793.19 882.948 2785.81 882.991 2782.37 883.066 2782.37 883.066 2778.86 883.142 2762.94 883.148 2762.94 883.148 2755.12 877.457 2751.44 872.018 2750.31 869.21 2750.32 862.713 2750.27 861.118 2750.21 859.523 2750.16 857.88 2755.97 839.546 2775.55 842.13 2791.15 842.436Z" fill="#000000" fill-rule="evenodd" fill-opacity="1"/><path d="M2871.29 688.254C2873.4 688.271 2875.52 688.289 2877.7 688.306 2884.43 688.373 2891.15 688.524 2897.88 688.678 2902.46 688.738 2907.03 688.793 2911.6 688.842 2922.79 688.975 2933.98 689.183 2945.17 689.43 2943.52 698.146 2941.3 705.891 2937.96 714.117 2936.7 717.265 2936.7 717.265 2935.41 720.477 2933.16 725.557 2933.16 725.557 2930.76 727.965 2924.64 728.298 2918.59 728.479 2912.47 728.539 2910.63 728.559 2908.79 728.578 2906.9 728.598 2902.99 728.632 2899.09 728.658 2895.19 728.676 2889.23 728.718 2883.27 728.823 2877.3 728.93 2873.52 728.954 2869.73 728.975 2865.94 728.991 2863.27 729.054 2863.27 729.054 2860.54 729.119 2852.23 729.084 2847.55 728.597 2840.77 723.602 2835.31 715.783 2835.62 710.791 2837.09 701.472 2847 687.804 2855.68 687.779 2871.29 688.254Z" fill="#000000" fill-rule="evenodd" fill-opacity="1"/><path d="M2759.73 687.731C2762.81 687.745 2765.88 687.759 2769.04 687.774 2794.67 688.115 2794.67 688.115 2802.8 693.658 2808.38 702.148 2808.81 705.554 2807.6 715.543 2803.12 721.142 2799.54 724.487 2793.19 727.701 2783.34 728.382 2773.49 728.464 2763.61 728.552 2760.34 728.592 2757.06 728.659 2753.78 728.752 2749.04 728.885 2744.31 728.93 2739.57 728.974 2736.74 729.022 2733.9 729.07 2730.98 729.12 2723.57 727.701 2723.57 727.701 2717.86 721.998 2713.05 714.015 2712.27 710.146 2713.97 700.953 2724.55 683.74 2741.54 687.256 2759.73 687.731Z" fill="#000000" fill-rule="evenodd" fill-opacity="1"/><path d="M2575.39 613.481C2580.44 613.501 2585.48 613.432 2590.53 613.356 2595.33 613.359 2595.33 613.359 2600.23 613.361 2603.15 613.356 2606.07 613.352 2609.08 613.347 2618.5 614.784 2622.54 617.399 2629.1 624.16 2630.47 632.567 2630.47 632.567 2629.1 640.974 2623.61 648.484 2620.67 650.381 2611.34 651.863 2606.63 651.903 2606.63 651.903 2601.83 651.943 2600.14 651.958 2598.46 651.972 2596.72 651.988 2593.17 652.005 2589.61 651.997 2586.06 651.967 2580.63 651.933 2575.21 652.015 2569.78 652.107 2566.32 652.109 2562.86 652.105 2559.4 652.093 2556.26 652.094 2553.12 652.096 2549.89 652.098 2541.82 650.582 2541.82 650.582 2535.88 645.027 2532.12 638.572 2532.12 638.572 2532.12 629.865 2538.23 609.439 2557.26 613.369 2575.39 613.481Z" fill="#000000" fill-rule="evenodd" fill-opacity="1"/><path d="M2896.04 1075.19C2899.73 1075.23 2899.73 1075.23 2903.49 1075.26 2907.31 1075.36 2907.31 1075.36 2911.21 1075.45 2915.09 1075.5 2915.09 1075.5 2919.05 1075.55 2925.42 1075.63 2931.8 1075.75 2938.17 1075.9 2934.18 1087.94 2930.18 1099.98 2926.19 1112.02 2918.8 1112.38 2911.41 1112.59 2904.02 1112.78 2901.93 1112.88 2899.84 1112.98 2897.68 1113.08 2882.19 1113.38 2882.19 1113.38 2874.4 1106.5 2869.84 1097.58 2870.66 1092.63 2873.46 1083.12 2880.75 1075.93 2886.01 1075.03 2896.04 1075.19Z" fill="#000000" fill-rule="evenodd" fill-opacity="1"/></g></g></svg>
......@@ -12,7 +12,9 @@ import { currentUser,
initLoading,
pollingInterval,
pollingTasks,
showAlert
showAlert,
logout,
login
} from './utils/other'
import { useI18n } from 'vue-i18n'
import Loading from './components/Loading.vue'
......@@ -74,7 +76,10 @@ onMounted(async () => {
localStorage.removeItem('currentUser')
isLoggedIn.value = false
console.log('Token已过期')
showAlert('请重新登录', 'warning')
showAlert('请重新登录', 'warning', {
label: t('login'),
onClick: login
})
}
} else {
isLoggedIn.value = false
......
......@@ -32,7 +32,7 @@ const router = useRouter();
<!-- Logo和标题 - Apple 风格 -->
<div class="text-center mb-12">
<div class="flex items-center justify-center gap-3 mb-5">
<i class="fas fa-film text-4xl text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i>
<img src="../../public/logo.svg" alt="LightX2V" class="w-14 h-12" loading="lazy" />
<h1 class="text-4xl font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">LightX2V</h1>
</div>
<p class="text-base text-[#86868b] dark:text-[#98989d] tracking-tight">{{ t('loginSubtitle') }}</p>
......
......@@ -331,19 +331,16 @@ watch(audioTemplates, (newTemplates) => {
<div v-for="(history, index) in audioHistory" :key="index"
@click="selectAudioHistory(history)"
class="flex items-center gap-4 p-4 rounded-2xl border border-black/8 dark:border-white/8 hover:border-[color:var(--brand-primary)]/50 dark:hover:border-[color:var(--brand-primary-light)]/50 transition-all cursor-pointer bg-white/80 dark:bg-[#2c2c2e]/80 hover:bg-white dark:hover:bg-[#3a3a3c] hover:shadow-[0_4px_12px_rgba(var(--brand-primary-rgb),0.15)] dark:hover:shadow-[0_4px_12px_rgba(var(--brand-primary-light-rgb),0.2)] group">
<div
class="w-12 h-12 bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 rounded-xl flex items-center justify-center flex-shrink-0">
<i class="fas fa-music text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] text-xl"></i>
<div class="w-12 h-12 rounded-xl overflow-hidden flex-shrink-0 bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 flex items-center justify-center">
<img v-if="history.imageUrl" :src="history.imageUrl" :alt="history.filename" class="w-full h-full object-cover" @error="history.imageUrl = null" />
<i v-else class="fas fa-music text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] text-xl"></i>
</div>
<div class="flex-1 min-w-0">
<div
class="text-[#1d1d1f] dark:text-[#f5f5f7] font-medium group-hover:text-[color:var(--brand-primary)] dark:group-hover:text-[color:var(--brand-primary-light)] transition-colors truncate tracking-tight">
{{ history.filename }}</div>
<div class="text-[#86868b] dark:text-[#98989d] text-sm flex items-center gap-2 tracking-tight">
<span>{{ t('audioFile') }}</span>
<span class="text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></span>
<span>{{ getDurationDisplay(history, false) }}</span>
</div>
<div class="text-[#86868b] dark:text-[#98989d] text-sm flex items-center gap-2 tracking-tight">
<span>{{ t('historyAudio') }}</span>
<span class="text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></span>
<span>{{ getDurationDisplay(history, false) }}</span>
</div>
</div>
<button @click.stop="handleAudioPreview(history, false)"
class="px-4 py-2 rounded-lg transition-all cursor-pointer relative z-10 flex items-center gap-2 flex-shrink-0 tracking-tight"
......@@ -416,21 +413,20 @@ watch(audioTemplates, (newTemplates) => {
</div>
</div>
<div v-if="audioTemplates.length > 0" class="space-y-3 px-1">
<div v-for="template in audioTemplates" :key="template.filename"
<div v-for="(template, index) in audioTemplates" :key="template.filename"
@click="selectAudioTemplate(template)"
class="flex items-center gap-4 p-4 rounded-2xl border border-black/8 dark:border-white/8 hover:border-[color:var(--brand-primary)]/50 dark:hover:border-[color:var(--brand-primary-light)]/50 transition-all cursor-pointer bg-white/80 dark:bg-[#2c2c2e]/80 hover:bg-white dark:hover:bg-[#3a3a3c] hover:shadow-[0_4px_12px_rgba(var(--brand-primary-rgb),0.15)] dark:hover:shadow-[0_4px_12px_rgba(var(--brand-primary-light-rgb),0.2)] group">
<div
class="w-12 h-12 bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 rounded-xl flex items-center justify-center flex-shrink-0">
<i class="fas fa-music text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] text-xl"></i>
class="w-12 h-12 rounded-xl overflow-hidden flex-shrink-0 bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 flex items-center justify-center">
<img v-if="imageTemplates[index]?.url" :src="imageTemplates[index].url" :alt="t('audioTemplates')" class="w-full h-full object-cover" @error="imageTemplates[index].url = null" />
<i v-else class="fas fa-music text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] text-xl"></i>
</div>
<div class="flex-1 min-w-0">
<div class="text-[#1d1d1f] dark:text-[#f5f5f7] font-medium group-hover:text-[color:var(--brand-primary)] dark:group-hover:text-[color:var(--brand-primary-light)] transition-colors truncate tracking-tight">{{ template.filename }}
</div>
<div class="text-[#86868b] dark:text-[#98989d] text-sm flex items-center gap-2 tracking-tight">
<span>{{ t('audioTemplates') }}</span>
<span class="text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></span>
<span>{{ getDurationDisplay(template, true) }}</span>
</div>
<div class="text-[#86868b] dark:text-[#98989d] text-sm flex items-center gap-2 tracking-tight">
<span>{{ t('audioTemplates') }}</span>
<span class="text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></span>
<span>{{ getDurationDisplay(template, true) }}</span>
</div>
</div>
<button @click.stop="handleAudioPreview(template, true)"
class="px-4 py-2 rounded-lg transition-all cursor-pointer relative z-10 flex items-center gap-2 flex-shrink-0 tracking-tight"
......
......@@ -517,13 +517,14 @@ watch([taskSearchQuery, statusFilter, currentTaskPage], () => {
<!-- 状态指示器 - Apple 风格 -->
<div class="absolute top-3 right-3">
<span :class="[
'px-3 py-1.5 rounded-full text-xs font-medium backdrop-blur-[20px] shadow-sm',
task.status === 'SUCCEED' ? 'bg-green-500/90 dark:bg-green-400/90 text-white' :
task.status === 'RUNNING' ? 'bg-[color:var(--brand-primary)]/90 dark:bg-[color:var(--brand-primary-light)]/90 text-white' :
task.status === 'FAILED' ? 'bg-red-500/90 dark:bg-red-400/90 text-white' :
'bg-white/90 dark:bg-[#2c2c2e]/90 text-[#86868b] dark:text-[#98989d] border border-black/8 dark:border-white/8'
]">
<span class="relative inline-flex items-center gap-2 px-3 py-2 rounded-full text-xs font-semibold tracking-tight shadow-[0_8px_24px_rgba(0,0,0,0.12)] backdrop-blur-[30px] bg-white/85 dark:bg-[#1f1f24]/85 text-[#1d1d1f] dark:text-[#f5f5f7] border border-white/60 dark:border-white/10">
<span class="inline-flex h-1.5 w-1.5 rounded-full"
:class="[
task.status === 'SUCCEED' ? 'bg-[#2ecc71]' :
task.status === 'RUNNING' ? 'bg-[#5865f2]' :
task.status === 'FAILED' ? 'bg-[#ff5a65]' :
'bg-[#a0a4b8]'
]"></span>
{{ getTaskStatusDisplay(task.status) }}
</span>
</div>
......
<template>
<!-- 模态框遮罩和容器 - Apple 极简风格 -->
<div class="fixed inset-0 bg-black/50 dark:bg-black/60 backdrop-blur-sm z-[60] flex items-center justify-center p-2 sm:p-1">
<div class="fixed inset-0 bg-black/50 dark:bg-black/60 backdrop-blur-sm z-[60] flex items-center justify-center p-2">
<div class="relative w-full h-full max-w-6xl max-h-[100vh] bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[40px] backdrop-saturate-[180%] border border-black/10 dark:border-white/10 rounded-3xl shadow-[0_20px_60px_rgba(0,0,0,0.2)] dark:shadow-[0_20px_60px_rgba(0,0,0,0.6)] overflow-hidden flex flex-col">
<!-- 模态框头部 - Apple 风格 -->
<div class="flex items-center justify-between px-6 py-4 border-b border-black/8 dark:border-white/8 bg-white/50 dark:bg-[#1e1e1e]/50 backdrop-blur-[20px] flex-shrink-0">
......
......@@ -90,7 +90,7 @@
"noPrompt": "No Prompt",
"uploadMaterials": "Upload Materials",
"image": "Image",
"audioFile": "Audio File",
"historyAudio": "History Audio",
"myProjects": "My Projects",
"initializationFailed": "Initialization Failed, Please Refresh The Page",
"browserNotSupported": "Browser Not Supported",
......@@ -342,10 +342,10 @@
"generatedAudio": "Generated Audio",
"synthesizedAudio": "Synthesized Audio",
"enterTextToConvert": "Enter text to convert",
"ttsPlaceholder": "Hello, how can I help you?",
"ttsPlaceholder": "The weather is nice today, let's go for a walk~",
"voiceInstruction": "Voice Instruction",
"voiceInstructionHint": "(Only for v2.0 voices)",
"voiceInstructionPlaceholder": "Use instruction to control voice details, including emotion, context, dialect, tone, speed, pitch, etc. Example: Say with a shy yet gentle tone",
"voiceInstructionPlaceholder": "Use instruction to control voice details, including emotion, context, dialect, tone, speed, pitch, etc. Example: Please use a warm and friendly voice",
"selectVoice": "Select Voice",
"searchVoice": "Search Voice",
"filter": "Filter",
......
......@@ -139,7 +139,7 @@
"image": "图片",
"shareTemplate": "分享模板",
"copyShareLink": "分享",
"audioFile": "音频文件",
"historyAudio": "历史音频",
"status": "状态",
"browserNotSupported": "您的浏览器不支持播放",
"videoLoadFailed": "视频加载失败",
......@@ -148,10 +148,10 @@
"succeed": "成功",
"success": "成功",
"failed": "失败",
"running": "运行",
"pending": "排队",
"running": "运行",
"pending": "排队",
"remaining": "剩余",
"cancelled": "取消",
"cancelled": "取消",
"all": "全部",
"reuseTask": "复用",
"regenerateTask": "重试",
......@@ -356,10 +356,10 @@
"generatedAudio": "生成的音频",
"synthesizedAudio": "合成音频",
"enterTextToConvert": "输入要转换的文本",
"ttsPlaceholder": "你好,请问我有什么可以帮您?",
"ttsPlaceholder": "今天的天气好好呀,要一起出去走走吗~",
"voiceInstruction": "语音指令",
"voiceInstructionHint": "(仅适用于v2.0音色)",
"voiceInstructionPlaceholder": "使用指令控制合成语音细节,包括但不限于情绪、语境、方言、语气、速度、音调等,例如:带点害羞又藏着温柔期待的语气说",
"voiceInstructionPlaceholder": "使用指令控制合成语音细节,包括但不限于情绪、语境、方言、语气、速度、音调等,例如:请用温暖亲切的声线介绍",
"selectVoice": "选择音色",
"searchVoice": "搜索音色",
"filter": "筛选",
......
......@@ -89,7 +89,7 @@ export const locale = i18n.global.locale
// 灵感广场分页相关变量
const inspirationPagination = ref(null);
const inspirationCurrentPage = ref(1);
const inspirationPageSize = ref(12);
const inspirationPageSize = ref(20);
const inspirationPageInput = ref(1);
const inspirationPaginationKey = ref(0);
......@@ -133,7 +133,7 @@ export const locale = i18n.global.locale
// Template分页相关变量
const templatePagination = ref(null);
const templateCurrentPage = ref(1);
const templatePageSize = ref(12); // 图片模板每页12个,音频模板每页10个
const templatePageSize = ref(20); // 图片模板每页12个,音频模板每页10个
const templatePageInput = ref(1);
const templatePaginationKey = ref(0);
const imageHistory = ref([]);
......@@ -154,7 +154,7 @@ export const locale = i18n.global.locale
const statusFilter = ref('ALL');
const pagination = ref(null);
const currentTaskPage = ref(1);
const taskPageSize = ref(12);
const taskPageSize = ref(20);
const taskPageInput = ref(1);
const paginationKey = ref(0); // 用于强制刷新分页组件
const taskMenuVisible = ref({}); // 管理每个任务的菜单显示状态
......@@ -638,7 +638,11 @@ export const locale = i18n.global.locale
});
if (response.status === 401) {
logout();
logout(false);
showAlert('认证失败,请重新登录', 'warning', {
label: t('login'),
onClick: login
});
throw new Error('认证失败,请重新登录');
}
if (response.status === 400) {
......@@ -728,6 +732,9 @@ export const locale = i18n.global.locale
if (response.ok) {
localStorage.setItem('accessToken', data.access_token);
if (data.refresh_token) {
localStorage.setItem('refreshToken', data.refresh_token);
}
localStorage.setItem('currentUser', JSON.stringify(data.user_info));
currentUser.value = data.user_info;
......@@ -819,6 +826,9 @@ export const locale = i18n.global.locale
const data = await response.json();
console.log(data);
localStorage.setItem('accessToken', data.access_token);
if (data.refresh_token) {
localStorage.setItem('refreshToken', data.refresh_token);
}
localStorage.setItem('currentUser', JSON.stringify(data.user_info));
currentUser.value = data.user_info;
isLoggedIn.value = true;
......@@ -864,9 +874,13 @@ export const locale = i18n.global.locale
}
};
const logout = () => {
let refreshPromise = null;
const logout = (showMessage = true) => {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('currentUser');
refreshPromise = null;
clearAllCache();
switchToLoginView();
......@@ -874,7 +888,9 @@ export const locale = i18n.global.locale
models.value = [];
tasks.value = [];
showAlert('已退出登录', 'info');
if (showMessage) {
showAlert('已退出登录', 'info');
}
};
const login = () => {
......@@ -1370,7 +1386,7 @@ export const locale = i18n.global.locale
} else if (error.name === 'NotFoundError') {
errorMessage = '未找到麦克风设备,请检查设备连接或使用其他设备';
} else if (error.name === 'NotSupportedError') {
errorMessage = '浏览器不支持录音功能,请使用Chrome、Firefox、Safari或Edge浏览器';
errorMessage = '移动端浏览器不支持录音功能,可以拍摄视频来代替录音';
} else if (error.name === 'NotReadableError') {
errorMessage = '麦克风被其他应用占用,请关闭其他使用麦克风的程序后重试';
} else if (error.name === 'OverconstrainedError') {
......@@ -2870,28 +2886,6 @@ export const locale = i18n.global.locale
const blob = fileInfo.blob;
const fileName = fileInfo.name || 'download';
const mimeType = blob.type || fileInfo.mimeType || 'application/octet-stream';
const isMobile = typeof navigator !== 'undefined' && /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
if (isMobile && typeof navigator?.canShare === 'function' && typeof navigator?.share === 'function') {
try {
const shareFile = new File([blob], fileName, { type: mimeType });
if (navigator.canShare({ files: [shareFile] })) {
await navigator.share({
files: [shareFile],
title: fileName
});
showAlert(t('downloadSuccessAlert'), 'success');
return true;
}
} catch (error) {
if (error?.name === 'AbortError') {
console.info('User cancelled share dialog');
showAlert(t('downloadCancelledAlert'), 'info');
return false;
}
console.warn('Native share failed, falling back to download link:', error);
}
}
try {
const objectUrl = URL.createObjectURL(blob);
......@@ -2960,49 +2954,6 @@ export const locale = i18n.global.locale
}
const blob = await response.blob();
const isMobileBrowser = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
if (isMobileBrowser) {
downloadLoadingMessage.value = '';
downloadLoading.value = false;
showAlert(t('mobileSaveToAlbumTip'), 'info');
const blobUrl = URL.createObjectURL(blob);
const previewWindow = window.open('', '_blank', 'noopener,noreferrer');
if (previewWindow) {
previewWindow.document.write(`<!DOCTYPE html>
<html lang="${locale.value || 'en'}">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>${t('mobileSavePreviewTitle')}</title>
<style>
body { margin: 0; background: #000; color: #fff; font-family: system-ui, sans-serif; }
.wrapper { min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 16px; gap: 16px; }
video { width: 100%; height: auto; border-radius: 16px; max-height: calc(100vh - 160px); }
p { text-align: center; line-height: 1.5; font-size: 15px; color: rgba(255,255,255,0.85); }
</style>
</head>
<body>
<div class="wrapper">
<video controls playsinline webkit-playsinline preload="auto" src="${blobUrl}"></video>
<p>${t('mobileSaveInstruction')}</p>
</div>
<script>
window.addEventListener('pagehide', () => URL.revokeObjectURL('${blobUrl}'));
window.addEventListener('beforeunload', () => URL.revokeObjectURL('${blobUrl}'));
</script>
</body>
</html>`);
previewWindow.document.close();
} else {
URL.revokeObjectURL(blobUrl);
window.location.href = downloadUrl;
}
return;
}
downloadLoadingMessage.value = t('downloadSaving');
await downloadFile({
blob,
......@@ -4201,10 +4152,9 @@ export const locale = i18n.global.locale
// 按时间戳排序,最新的在前
uniqueImages.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
const result = uniqueImages.slice(0, 20); // 只显示最近20条
imageHistory.value = result;
console.log('从任务列表获取图片历史:', result.length, '');
return result;
imageHistory.value = uniqueImages;
console.log('从任务列表获取图片历史:', uniqueImages.length, '');
return uniqueImages;
} catch (error) {
console.error('获取图片历史失败:', error);
imageHistory.value = [];
......@@ -4228,13 +4178,15 @@ export const locale = i18n.global.locale
if (task.inputs && task.inputs.input_audio && !seenAudios.has(task.inputs.input_audio)) {
// 获取音频URL
const audioUrl = await getTaskFileUrl(task.task_id, 'input_audio');
const imageUrl = task.inputs.input_image ? await getTaskFileUrl(task.task_id, 'input_image') : null;
if (audioUrl) {
uniqueAudios.push({
filename: task.inputs.input_audio,
url: audioUrl,
taskId: task.task_id,
timestamp: task.create_t,
taskType: task.task_type
taskType: task.task_type,
imageUrl
});
seenAudios.add(task.inputs.input_audio);
}
......@@ -4244,10 +4196,9 @@ export const locale = i18n.global.locale
// 按时间戳排序,最新的在前
uniqueAudios.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
const result = uniqueAudios.slice(0, 20); // 只显示最近20条
audioHistory.value = result;
console.log('从任务列表获取音频历史:', result.length, '');
return result;
audioHistory.value = uniqueAudios;
console.log('从任务列表获取音频历史:', uniqueAudios.length, '');
return uniqueAudios;
} catch (error) {
console.error('获取音频历史失败:', error);
audioHistory.value = [];
......@@ -4400,6 +4351,7 @@ export const locale = i18n.global.locale
try {
// 清理任务历史
localStorage.removeItem('taskHistory');
localStorage.removeItem('refreshToken');
// 清理其他可能的缓存数据
const keysToRemove = [];
......@@ -4460,8 +4412,59 @@ export const locale = i18n.global.locale
}
};
const refreshAccessToken = async () => {
if (refreshPromise) {
return refreshPromise;
}
const refreshToken = localStorage.getItem('refreshToken');
if (!refreshToken) {
return false;
}
refreshPromise = (async () => {
try {
const response = await fetch('/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ refresh_token: refreshToken })
});
await new Promise(resolve => setTimeout(resolve, 100));
if (!response.ok) {
throw new Error(`Refresh failed with status ${response.status}`);
}
const data = await response.json();
if (data.access_token) {
localStorage.setItem('accessToken', data.access_token);
}
if (data.refresh_token) {
localStorage.setItem('refreshToken', data.refresh_token);
}
if (data.user_info) {
currentUser.value = data.user_info;
localStorage.setItem('currentUser', JSON.stringify(data.user_info));
}
return true;
} catch (error) {
console.error('Refresh token failed:', error);
logout(false);
showAlert('登录已过期,请重新登录', 'warning', {
label: t('login'),
onClick: login
});
return false;
} finally {
refreshPromise = null;
}
})();
return refreshPromise;
};
// 增强的API请求函数,自动处理认证错误
const apiRequest = async (url, options = {}) => {
const apiRequest = async (url, options = {}, allowRetry = true) => {
const headers = getAuthHeaders();
try {
......@@ -4474,12 +4477,14 @@ export const locale = i18n.global.locale
});
await new Promise(resolve => setTimeout(resolve, 100));
// 检查是否是认证错误
if (response.status === 401 || response.status === 403) {
// Token无效,清除本地存储并跳转到登录页
logout();
showAlert('登录已过期,请重新登录', 'warning');
if ((response.status === 401 || response.status === 403) && allowRetry) {
const refreshed = await refreshAccessToken();
if (refreshed) {
return await apiRequest(url, options, false);
}
return null;
}
return response;
} catch (error) {
console.error('API request failed:', error);
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment