├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── accounts.yml.example ├── app ├── core │ ├── account_manager.py │ ├── config_manager.py │ ├── cookie_service.py │ ├── logger │ │ ├── logger.py │ │ └── logger_config.py │ └── security.py ├── main.py ├── models │ ├── account.py │ ├── chat.py │ └── model.py ├── router │ ├── account.py │ ├── chat.py │ └── model.py └── service │ ├── account_service.py │ ├── completion_service.py │ ├── message_service.py │ ├── model_service.py │ ├── task_service.py │ └── upload_service.py ├── config.yaml.example ├── pyproject.toml ├── requirements.txt ├── run.py ├── static ├── 404 │ └── index.html ├── 404.html ├── _next │ └── static │ │ ├── BXR8W7u2Bte2AoAKInO7B │ │ ├── _buildManifest.js │ │ └── _ssgManifest.js │ │ ├── chunks │ │ ├── 205-e9916776d75a144a.js │ │ ├── 212-ef89a1d425b5c141.js │ │ ├── 245-836a16859841a26d.js │ │ ├── 250-1326d26798efe2a8.js │ │ ├── 385-76a53e116716a735.js │ │ ├── 492-32c3c267e02b55a9.js │ │ ├── 513-880d3283fb3daaab.js │ │ ├── 518-ae5fe8168930f90d.js │ │ ├── 587-ddfc51f5a9d66e65.js │ │ ├── 610-996bd65a0cbf6dd7.js │ │ ├── 633-4697634c55b84236.js │ │ ├── 69-e9dd151ce9c720b2.js │ │ ├── 95-06814ef9f06ba32c.js │ │ ├── app │ │ │ ├── _not-found-cf5c4a63df6f8e87.js │ │ │ ├── account │ │ │ │ └── login │ │ │ │ │ └── page-011af162c7b9cfe7.js │ │ │ ├── admin │ │ │ │ ├── cookies │ │ │ │ │ └── page-2a3752622a163bc5.js │ │ │ │ ├── layout-4edb65843458ac27.js │ │ │ │ └── list │ │ │ │ │ └── page-72d2f77d5b74356a.js │ │ │ ├── layout-396a030f32c8d339.js │ │ │ └── page-70327e014d79b0e2.js │ │ ├── fd9d1056-5f0ed9a2c5ded3c0.js │ │ ├── framework-f66176bb897dc684.js │ │ ├── main-app-2552f5e9a5d47588.js │ │ ├── main-c8d5b42f05d9bcf1.js │ │ ├── pages │ │ │ ├── _app-75f6107b0260711c.js │ │ │ └── _error-9a890acb1e81c3fc.js │ │ ├── polyfills-c67a75d1b6f99dc8.js │ │ └── webpack-909dbc96a8209865.js │ │ └── css │ │ ├── 287f887716c32fb8.css │ │ └── e99d04f548050b4e.css ├── account │ └── login │ │ ├── index.html │ │ └── index.txt ├── admin │ ├── cookies │ │ ├── index.html │ │ └── index.txt │ └── list │ │ ├── index.html │ │ └── index.txt ├── index.html └── index.txt └── uv.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Virtual Environment 24 | venv/ 25 | ENV/ 26 | env/ 27 | 28 | # IDE 29 | .idea/ 30 | .vscode/ 31 | *.swp 32 | *.swo 33 | 34 | # Logs 35 | logs/ 36 | *.log 37 | 38 | # Local config 39 | config.yaml 40 | accounts.yml 41 | 42 | # Git 43 | .git/ 44 | .gitignore 45 | 46 | # Docker 47 | .docker/ 48 | Dockerfile 49 | docker-compose.yml 50 | 51 | # Tests 52 | tests/ 53 | test_* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | <<<<<<< HEAD 2 | 3 | # Created by https://www.toptal.com/developers/gitignore/api/python 4 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 5 | 6 | ### Python ### 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | pip-wheel-metadata/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | pytestdebug.log 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | doc/_build/ 81 | 82 | # PyBuilder 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 103 | __pypackages__/ 104 | 105 | # Celery stuff 106 | celerybeat-schedule 107 | celerybeat.pid 108 | 109 | # SageMath parsed files 110 | *.sage.py 111 | 112 | # Environments 113 | .env 114 | .venv 115 | env/ 116 | venv/ 117 | ENV/ 118 | env.bak/ 119 | venv.bak/ 120 | 121 | # Spyder project settings 122 | .spyderproject 123 | .spyproject 124 | 125 | # Rope project settings 126 | .ropeproject 127 | 128 | # mkdocs documentation 129 | /site 130 | 131 | # mypy 132 | .mypy_cache/ 133 | .dmypy.json 134 | dmypy.json 135 | 136 | # Pyre type checker 137 | .pyre/ 138 | 139 | # pytype static type analyzer 140 | .pytype/ 141 | 142 | # End of https://www.toptal.com/developers/gitignore/api/python 143 | /config 144 | /plugins/禁用 145 | *.lnk 146 | *.bat 147 | 148 | 149 | # 忽略所有.env.xxx文件 150 | *.dev 151 | *.prod 152 | *.sh 153 | # 忽略根目录下的所有.xxx文件夹 154 | /cache/ 155 | /data/ 156 | ======= 157 | 158 | # Created by https://www.toptal.com/developers/gitignore/api/python 159 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 160 | 161 | ### Python ### 162 | # Byte-compiled / optimized / DLL files 163 | __pycache__/ 164 | *.py[cod] 165 | *$py.class 166 | 167 | # C extensions 168 | *.so 169 | 170 | # Distribution / packaging 171 | .Python 172 | build/ 173 | develop-eggs/ 174 | dist/ 175 | downloads/ 176 | eggs/ 177 | .eggs/ 178 | lib/ 179 | lib64/ 180 | parts/ 181 | sdist/ 182 | var/ 183 | wheels/ 184 | pip-wheel-metadata/ 185 | share/python-wheels/ 186 | *.egg-info/ 187 | .installed.cfg 188 | *.egg 189 | MANIFEST 190 | 191 | # PyInstaller 192 | # Usually these files are written by a python script from a template 193 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 194 | *.manifest 195 | *.spec 196 | 197 | # Installer logs 198 | pip-log.txt 199 | pip-delete-this-directory.txt 200 | 201 | # Unit test / coverage reports 202 | htmlcov/ 203 | .tox/ 204 | .nox/ 205 | .coverage 206 | .coverage.* 207 | .cache 208 | nosetests.xml 209 | coverage.xml 210 | *.cover 211 | *.py,cover 212 | .hypothesis/ 213 | .pytest_cache/ 214 | pytestdebug.log 215 | 216 | # Translations 217 | *.mo 218 | *.pot 219 | 220 | # Django stuff: 221 | *.log 222 | local_settings.py 223 | db.sqlite3 224 | db.sqlite3-journal 225 | 226 | # Flask stuff: 227 | instance/ 228 | .webassets-cache 229 | 230 | # Scrapy stuff: 231 | .scrapy 232 | 233 | # Sphinx documentation 234 | docs/_build/ 235 | doc/_build/ 236 | 237 | # PyBuilder 238 | target/ 239 | 240 | # Jupyter Notebook 241 | .ipynb_checkpoints 242 | 243 | # IPython 244 | profile_default/ 245 | ipython_config.py 246 | 247 | # pyenv 248 | .python-version 249 | 250 | # pipenv 251 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 252 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 253 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 254 | # install all needed dependencies. 255 | #Pipfile.lock 256 | 257 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 258 | __pypackages__/ 259 | 260 | # Celery stuff 261 | celerybeat-schedule 262 | celerybeat.pid 263 | 264 | # SageMath parsed files 265 | *.sage.py 266 | 267 | # Environments 268 | .env 269 | .venv 270 | env/ 271 | venv/ 272 | ENV/ 273 | env.bak/ 274 | venv.bak/ 275 | 276 | # Spyder project settings 277 | .spyderproject 278 | .spyproject 279 | 280 | # Rope project settings 281 | .ropeproject 282 | 283 | # mkdocs documentation 284 | /site 285 | 286 | # mypy 287 | .mypy_cache/ 288 | .dmypy.json 289 | dmypy.json 290 | 291 | # Pyre type checker 292 | .pyre/ 293 | 294 | # pytype static type analyzer 295 | .pytype/ 296 | 297 | # End of https://www.toptal.com/developers/gitignore/api/python 298 | /config 299 | /plugins/禁用 300 | *.lnk 301 | *.bat 302 | 303 | 304 | # 忽略所有.env.xxx文件 305 | *.dev 306 | *.prod 307 | *.sh 308 | *.yml 309 | *.yaml 310 | # 忽略根目录下的所有.xxx文件夹 311 | /cache/ 312 | /data/ 313 | /sync/ 314 | /.vscode/ 315 | /.idea 316 | desktop.int -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.13-slim 2 | 3 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ 4 | 5 | COPY . /qwen2api 6 | 7 | WORKDIR /qwen2api 8 | RUN uv sync --frozen --no-cache 9 | 10 | # Run the application. 11 | 12 | CMD ["/qwen2api/.venv/bin/python", "run.py"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Qwen2API 2 | 3 | 通义千问 API 的Python版本实现,基于FastAPI和httpx。 4 | 5 | > [!NOTE] 6 | > 现已支持前端管理,访问 **/static** 页面即可查看。 7 | 8 | > [!IMPORTANT] 9 | > 请将原本的配置文件 **accounts.yml** 和 **config.yaml** 从主目录移到到 **config** 目录下 10 | 11 | > [!IMPORTANT] 12 | > 数据已移动至 **data** 目录下存储,包括 13 | > - model.json 模型配置文件 14 | > - upload.json 缓存的图片 SHA256 URL 对应关系 15 | 16 | ## 功能特点 17 | 18 | - [X] 支持通义千问全部模型 19 | - [X] 支持流式输出 20 | - [X] 支持思考过程显示 21 | - [X] 支持搜索功能 22 | - [X] 支持图像生成 23 | - [X] 支持视频生成 24 | - [X] 支持多账户轮询调度 25 | - [X] 支持缓存上传图片URL,无需等待过久 26 | - [X] 前端管理 27 | - [X] Docker镜像支持(部分) 28 | 29 | ## 环境要求 30 | 31 | - Python 3.7+ 32 | - FastAPI 33 | - Uvicorn 34 | - httpx 35 | - python-multipart 36 | 37 | ## 快速开始 38 | 39 | ### 1. 安装依赖 40 | 41 | ```bash 42 | pip install -r requirements.txt 43 | ``` 44 | 45 | ### 2. 配置文件 46 | 47 | > [!IMPORTANT] 48 | > 配置文件现在不会自动生成。 49 | > 请复制 config.yaml.example 去除 example 后缀,并修改,得到你的配置文件 config.yaml ,**放入 config 文件夹中** 50 | 51 | ### 3. 启动服务 52 | 53 | ```bash 54 | python run.py 55 | ``` 56 | ## Docker 命令 57 | ``` 58 | docker build -t qwen2api . 59 | docker run -d \ 60 | --name qwen2api \ 61 | -v $(pwd)/config:/qwen2api/config \ 62 | -v $(pwd)/data:/qwen2api/data \ 63 | -v $(pwd)/logs:/qwen2api/logs \ 64 | -p 2778:2778 \ 65 | qwen2api:latest 66 | ``` 67 | ## API接口 68 | 69 | 提供与 OpenAI 兼容的接口 70 | 71 | ## 高级功能 72 | 73 | ### 思考过程 74 | 75 | 在模型名称后添加`-thinking`后缀,例如:`qwen-max-latest-thinking`。 76 | 77 | ### 网络搜索 78 | 79 | 在模型名称后添加`-search`后缀,例如:`qwen-max-latest-search`。 80 | 81 | ### 套娃 82 | 83 | 在模型名称后添加`-thinking-search`后缀,例如:`qwen-max-latest-thinking-search`。 84 | 85 | ### 图像生成 86 | 87 | 在模型名称后添加`-draw`后缀,例如:`qwen-max-latest-draw`。 88 | 89 | ### 视频生成 90 | 91 | 在模型名称后添加`-video`后缀,例如:`qwen-max-latest-video`。 92 | (注意Cherry Studio无法正常显示🫥) 93 | 94 | 95 | ## 免责声明 96 | 97 | 本项目仅供学习和研究使用,不构成任何商业用途。使用本项目所产生的任何直接或间接的法律责任由使用者自行承担。本项目不对使用者的任何行为负责。 98 | 99 | ## 许可证 100 | 101 | MIT License 102 | 103 | #### 自用 104 | > 导出依赖 105 | >```bash 106 | >pipdeptree --warn silence | Select-String -Pattern '^\w+' > .\requirements.txt 107 | >``` -------------------------------------------------------------------------------- /accounts.yml.example: -------------------------------------------------------------------------------- 1 | accounts: 2 | - cookie: *** 3 | enabled: true 4 | expires_at: 1745559106 5 | password: *** 6 | token: *** 7 | username: *** 8 | -------------------------------------------------------------------------------- /app/core/account_manager.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | from datetime import datetime 3 | from typing import Dict, List, Optional, Union 4 | import os 5 | from pathlib import Path 6 | from app.core.logger.logger import get_logger 7 | logger = get_logger(__name__) 8 | 9 | class AccountManager: 10 | def __init__(self, config_path: str = "config/accounts.yml"): 11 | """ 12 | 初始化账号管理器 13 | Args: 14 | config_path: YAML配置文件的路径 15 | """ 16 | self.config_path = config_path 17 | self.accounts = [] 18 | self.common_cookies = {} 19 | self.load_accounts() 20 | 21 | def load_accounts(self) -> None: 22 | 23 | """加载账号配置文件""" 24 | if not os.path.exists(self.config_path): 25 | self.save_accounts() 26 | return 27 | 28 | with open(self.config_path, 'r', encoding='utf-8') as f: 29 | data = yaml.safe_load(f) or {} 30 | self.accounts = data.get('accounts', []) 31 | self.common_cookies = data.get('common_cookies', {}) 32 | 33 | def save_accounts(self) -> None: 34 | """保存账号配置到文件,并同步reload到内存""" 35 | # 确保目录存在 36 | os.makedirs(os.path.dirname(self.config_path), exist_ok=True) 37 | 38 | data = { 39 | 'accounts': self.accounts, 40 | 'common_cookies': self.common_cookies 41 | } 42 | 43 | with open(self.config_path, 'w', encoding='utf-8') as f: 44 | yaml.safe_dump(data, f, allow_unicode=True) 45 | # 关键点:每次写盘后reload一遍内存副本 46 | self.load_accounts() 47 | 48 | def _extract_token_from_cookie(self, cookie: str) -> Optional[str]: 49 | """ 50 | 从cookie字符串中提取token 51 | Args: 52 | cookie: cookie字符串 53 | Returns: 54 | Optional[str]: 提取到的token,如果未找到则返回None 55 | """ 56 | # 分割cookie字符串 57 | cookie_parts = cookie.split(',') 58 | for part in cookie_parts: 59 | if 'token=' in part: 60 | # 提取token值 61 | token = part.split('token=')[1].split(';')[0] 62 | return token 63 | return None 64 | 65 | def add_account(self, username: str, password: str) -> Dict: 66 | """ 67 | 添加新账号(仅需用户名和密码) 68 | Args: 69 | username: 用户名 70 | password: 密码 71 | Returns: 72 | Dict: 创建的账号基础信息 73 | """ 74 | # 检查是否已存在相同用户名的账号 75 | if self.get_account_by_username(username): 76 | raise ValueError(f"用户名 {username} 已存在") 77 | 78 | # 创建基础账号信息 79 | account = { 80 | "username": username, 81 | "password": password, 82 | "enabled": True 83 | } 84 | 85 | # 加到 accounts 并同步保存 86 | self.accounts.append(account) 87 | self.save_accounts() 88 | return account 89 | 90 | def complete_account_info(self, username: str, cookie: str, expires_at: int) -> bool: 91 | """ 92 | 完成账号信息的添加(登录后调用) 93 | Args: 94 | username: 用户名 95 | cookie: 登录后获取的cookie 96 | expires_at: 过期时间戳 97 | Returns: 98 | bool: 是否成功完成账号信息添加 99 | """ 100 | account = self.get_account_by_username(username) 101 | if not account: 102 | return False 103 | 104 | # 从cookie中提取token 105 | token = self._extract_token_from_cookie(cookie) 106 | if not token: 107 | return False 108 | 109 | # 更新账号信息 110 | account.update({ 111 | "cookie": cookie, 112 | "token": token, 113 | "expires_at": expires_at 114 | }) 115 | 116 | self.save_accounts() 117 | return True 118 | 119 | def get_account_by_token(self, token: str) -> Optional[Dict]: 120 | self.load_accounts() 121 | """通过token查找账号""" 122 | for account in self.accounts: 123 | if account.get('token') == token: 124 | return account 125 | return None 126 | 127 | def update_account(self, username: str, updates: Dict) -> bool: 128 | """ 129 | 更新账号信息 130 | Args: 131 | username: 要更新的账号的用户名 132 | updates: 要更新的字段和值 133 | Returns: 134 | bool: 是否更新成功 135 | """ 136 | for i, account in enumerate(self.accounts): 137 | if account['username'] == username: 138 | self.accounts[i].update(updates) 139 | self.save_accounts() 140 | return True 141 | return False 142 | 143 | def delete_account(self, username: str) -> bool: 144 | """ 145 | 删除账号 146 | Args: 147 | username: 要删除的账号的用户名 148 | Returns: 149 | bool: 是否删除成功 150 | """ 151 | initial_length = len(self.accounts) 152 | self.accounts = [acc for acc in self.accounts if acc['username'] != username] 153 | 154 | if len(self.accounts) < initial_length: 155 | self.save_accounts() 156 | return True 157 | return False 158 | 159 | def get_account_by_username(self, username: str) -> Optional[Dict]: 160 | """ 161 | 通过用户名查找账号 162 | Args: 163 | username: 要查找的用户名 164 | Returns: 165 | Optional[Dict]: 找到的账号信息,未找到返回None 166 | """ 167 | self.load_accounts() 168 | for account in self.accounts: 169 | if account['username'] == username: 170 | return account 171 | return None 172 | 173 | def get_enabled_accounts(self) -> List[Dict]: 174 | """获取所有启用的账号""" 175 | self.load_accounts() 176 | return [acc for acc in self.accounts if acc['enabled']] 177 | 178 | def get_valid_accounts(self) -> List[Dict]: 179 | """获取所有未过期的账号""" 180 | self.load_accounts() 181 | now = datetime.now().timestamp() 182 | return [acc for acc in self.accounts if acc['expires_at'] > now] 183 | 184 | def get_common_cookies(self) -> Dict: 185 | """获取通用cookies""" 186 | self.load_accounts() 187 | return self.common_cookies 188 | 189 | def update_common_cookies(self, cookies: Dict) -> None: 190 | """更新通用cookies""" 191 | self.common_cookies = cookies 192 | self.save_accounts() 193 | 194 | def get_all_accounts(self) -> List[Dict]: 195 | """获取所有账号""" 196 | self.load_accounts() 197 | return self.accounts -------------------------------------------------------------------------------- /app/core/config_manager.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | from typing import Any, Dict, List, Optional, Union 3 | import os 4 | from pathlib import Path 5 | from copy import deepcopy 6 | from loguru import logger 7 | 8 | class ConfigManager: 9 | def __init__(self, config_path: str = "config/config.yaml"): 10 | """ 11 | 初始化配置管理器 12 | 13 | Args: 14 | config_path: 配置文件路径 15 | """ 16 | self.config_path = config_path 17 | self.config = {} 18 | self.load_config() 19 | 20 | def load_config(self) -> None: 21 | """加载配置文件""" 22 | if not os.path.exists(self.config_path): 23 | logger.error(f"配置文件不存在: {self.config_path}") 24 | raise FileNotFoundError(f"配置文件不存在: {self.config_path}") 25 | 26 | with open(self.config_path, 'r', encoding='utf-8') as f: 27 | self.config = yaml.safe_load(f) 28 | if not self.config: 29 | logger.error(f"配置文件为空: {self.config_path}") 30 | raise ValueError(f"配置文件为空: {self.config_path}") 31 | 32 | def save_config(self) -> None: 33 | """保存配置到文件""" 34 | try: 35 | os.makedirs(os.path.dirname(self.config_path), exist_ok=True) 36 | with open(self.config_path, 'w', encoding='utf-8') as f: 37 | yaml.safe_dump(self.config, f, allow_unicode=True) 38 | logger.info(f"配置已保存到: {self.config_path}") 39 | except Exception as e: 40 | logger.error(f"保存配置失败: {str(e)}") 41 | raise 42 | 43 | def get(self, path: str, default: Any = None) -> Any: 44 | """ 45 | 获取配置值 46 | 47 | Args: 48 | path: 配置路径,使用点号分隔,如 'api.host' 49 | default: 默认值,如果配置项不存在时返回默认值 50 | Returns: 51 | Any: 配置值 52 | 53 | Raises: 54 | KeyError: 配置项不存在时抛出异常 55 | """ 56 | keys = path.split('.') 57 | value = self.config 58 | 59 | for key in keys: 60 | if not isinstance(value, dict): 61 | logger.error(f"配置路径无效: {path}") 62 | raise KeyError(f"配置路径无效: {path}") 63 | 64 | if key not in value and default is not None: 65 | return default 66 | 67 | value = value[key] 68 | 69 | return value 70 | 71 | def set(self, path: str, value: Any) -> None: 72 | """ 73 | 设置配置值 74 | 75 | Args: 76 | path: 配置路径,使用点号分隔,如 'api.host' 77 | value: 要设置的值 78 | """ 79 | keys = path.split('.') 80 | config = self.config 81 | 82 | # 遍历到最后一个键之前 83 | for key in keys[:-1]: 84 | if key not in config: 85 | config[key] = {} 86 | elif not isinstance(config[key], dict): 87 | logger.error(f"配置路径无效: {path}") 88 | raise KeyError(f"配置路径无效: {path}") 89 | config = config[key] 90 | 91 | # 设置最后一个键的值 92 | config[keys[-1]] = value 93 | self.save_config() 94 | logger.info(f"已更新配置: {path} = {value}") 95 | 96 | def delete(self, path: str) -> None: 97 | """ 98 | 删除配置项 99 | 100 | Args: 101 | path: 配置路径,使用点号分隔,如 'api.host' 102 | 103 | Raises: 104 | KeyError: 配置项不存在时抛出异常 105 | """ 106 | keys = path.split('.') 107 | config = self.config 108 | 109 | # 遍历到最后一个键之前 110 | for key in keys[:-1]: 111 | if key not in config: 112 | logger.error(f"配置项不存在: {path}") 113 | raise KeyError(f"配置项不存在: {path}") 114 | config = config[key] 115 | 116 | # 删除最后一个键 117 | if keys[-1] not in config: 118 | logger.error(f"配置项不存在: {path}") 119 | raise KeyError(f"配置项不存在: {path}") 120 | 121 | del config[keys[-1]] 122 | self.save_config() 123 | logger.info(f"已删除配置项: {path}") 124 | 125 | def get_section(self, section: str) -> Dict: 126 | """ 127 | 获取整个配置部分 128 | 129 | Args: 130 | section: 配置部分名称,如 'api' 131 | 132 | Returns: 133 | Dict: 配置部分的内容 134 | 135 | Raises: 136 | KeyError: 配置部分不存在时抛出异常 137 | """ 138 | if section not in self.config: 139 | logger.error(f"配置部分不存在: {section}") 140 | raise KeyError(f"配置部分不存在: {section}") 141 | return dict(self.config[section]) 142 | 143 | def update_section(self, section: str, values: Dict) -> None: 144 | """ 145 | 更新配置部分 146 | 147 | Args: 148 | section: 配置部分名称,如 'api' 149 | values: 要更新的值 150 | """ 151 | if section not in self.config: 152 | self.config[section] = {} 153 | 154 | def deep_update(d: Dict, u: Dict) -> None: 155 | for k, v in u.items(): 156 | if isinstance(v, dict) and k in d and isinstance(d[k], dict): 157 | deep_update(d[k], v) 158 | else: 159 | d[k] = v 160 | 161 | deep_update(self.config[section], values) 162 | self.save_config() 163 | logger.info(f"已更新配置部分: {section}") 164 | 165 | def get_all(self) -> Dict: 166 | """ 167 | 获取所有配置 168 | 169 | Returns: 170 | Dict: 所有配置的副本 171 | """ 172 | return dict(self.config) 173 | 174 | def exists(self, path: str) -> bool: 175 | """ 176 | 检查配置项是否存在 177 | 178 | Args: 179 | path: 配置路径,使用点号分隔,如 'api.host' 180 | 181 | Returns: 182 | bool: 配置项是否存在 183 | """ 184 | try: 185 | self.get(path) 186 | return True 187 | except KeyError: 188 | return False -------------------------------------------------------------------------------- /app/core/cookie_service.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Optional, Tuple 2 | from .account_manager import AccountManager 3 | import random 4 | class CookieService: 5 | def __init__(self, account_manager: AccountManager): 6 | """ 7 | 初始化Cookie服务 8 | 9 | Args: 10 | account_manager: AccountManager实例,用于获取cookies 11 | """ 12 | self.account_manager = account_manager 13 | 14 | def _get_default_account(self) -> Tuple[str, str]: 15 | """ 16 | 获取默认账号的token和cookie 17 | 18 | Returns: 19 | Tuple[str, str]: (token, cookie)元组 20 | """ 21 | # 获取有效账号列表 22 | valid_accounts = self.account_manager.get_valid_accounts() 23 | if not valid_accounts: 24 | return '', '' 25 | 26 | # 使用第一个有效账号 27 | account = valid_accounts[0] 28 | return account.get('token', ''), account.get('cookie', '') 29 | 30 | def _merge_cookies(self, custom_cookie: Optional[str] = None) -> str: 31 | """ 32 | 合并通用cookies和自定义cookie 33 | 34 | Args: 35 | custom_cookie: 可选的自定义cookie字符串 36 | 37 | Returns: 38 | str: 合并后的cookie字符串 39 | """ 40 | cookie_parts = [] 41 | 42 | # 添加自定义cookie 43 | if custom_cookie: 44 | cookie_parts.append(custom_cookie) 45 | 46 | # 添加通用cookies 47 | common_cookies = self.account_manager.get_common_cookies() 48 | if common_cookies: 49 | common_cookie_str = '; '.join([f'{k}={v}' for k, v in common_cookies.items()]) 50 | cookie_parts.append(common_cookie_str) 51 | 52 | # 合并所有cookie 53 | return '; '.join(cookie_parts) 54 | 55 | def get_headers(self, auth_token: Optional[str] = None, custom_cookie: Optional[str] = None) -> Dict[str, str]: 56 | """ 57 | 获取请求头 58 | 59 | Args: 60 | auth_token: 可选的认证Token 61 | custom_cookie: 可选的自定义cookie字符串 62 | 63 | Returns: 64 | Dict[str, str]: 完整的请求头字典 65 | """ 66 | # 如果没有提供token和cookie,使用默认账号 67 | if auth_token is None and custom_cookie is None: 68 | auth_token, custom_cookie = self._get_default_account() 69 | # 如果只提供了token,尝试查找对应的cookie 70 | elif auth_token and not custom_cookie: 71 | account = self.account_manager.get_account_by_token(auth_token) 72 | if account: 73 | custom_cookie = account.get('cookie', '') 74 | 75 | # 基础请求头 76 | headers = { 77 | "accept": "application/json", 78 | "accept-language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", 79 | "accept-encoding": "gzip", 80 | "content-type": "application/json", 81 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0", 82 | "origin": "https://chat.qwen.ai", 83 | "referer": "https://chat.qwen.ai/", 84 | "dnt": "1", 85 | "sec-gpc": "1", 86 | "connection": "keep-alive", 87 | "source": "web", 88 | "Sec-Fetch-Dest": "empty", 89 | "Sec-Fetch-Mode": "cors", 90 | "Sec-Fetch-Site": "same-origin", 91 | "Priority": "u=4", 92 | "TE": "trailers", 93 | "Pragma": "no-cache", 94 | "Cache-Control": "no-cache" 95 | } 96 | 97 | # 添加认证token 98 | if auth_token: 99 | headers["authorization"] = f"Bearer {auth_token}" 100 | 101 | # 合并并添加cookies 102 | merged_cookies = self._merge_cookies(custom_cookie) 103 | if merged_cookies: 104 | headers["cookie"] = merged_cookies 105 | 106 | return headers 107 | def get_auth_token(self) -> str: 108 | """ 109 | 从所有账号中随机获取一个token 110 | 111 | Returns: 112 | str: 随机选择的认证Token,如果没有可用token则返回空字符串 113 | """ 114 | accounts = self.account_manager.get_all_accounts() 115 | # 直接随机选择一个账号,然后检查是否有token,避免创建新列表 116 | if accounts: 117 | account = random.choice(accounts) 118 | return account.get('token', '') 119 | return '' 120 | -------------------------------------------------------------------------------- /app/core/logger/logger.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | from pathlib import Path 4 | from typing import Union, Any, Protocol, runtime_checkable 5 | from loguru import logger as loguru_logger 6 | from loguru._logger import Logger 7 | 8 | @runtime_checkable 9 | class LoggerProtocol(Protocol): 10 | """日志记录器协议,定义了日志记录器应该具有的方法""" 11 | def debug(self, __message: str, *args: Any, **kwargs: Any) -> None: ... 12 | def info(self, __message: str, *args: Any, **kwargs: Any) -> None: ... 13 | def warning(self, __message: str, *args: Any, **kwargs: Any) -> None: ... 14 | def error(self, __message: str, *args: Any, **kwargs: Any) -> None: ... 15 | def critical(self, __message: str, *args: Any, **kwargs: Any) -> None: ... 16 | def exception(self, __message: str, *args: Any, **kwargs: Any) -> None: ... 17 | def log(self, __level: str, __message: str, *args: Any, **kwargs: Any) -> None: ... 18 | def bind(self, **kwargs: Any) -> Any: ... 19 | 20 | class InterceptHandler(logging.Handler): 21 | """ 22 | 将标准 logging 的日志重定向到 loguru 23 | """ 24 | def emit(self, record: logging.LogRecord) -> None: 25 | try: 26 | level = loguru_logger.level(record.levelname).name 27 | except ValueError: 28 | level = record.levelno 29 | 30 | frame, depth = sys._getframe(6), 6 31 | while frame and frame.f_code.co_filename == logging.__file__: 32 | frame = frame.f_back 33 | depth += 1 34 | 35 | loguru_logger.opt(depth=depth, exception=record.exc_info).log( 36 | level, record.getMessage() 37 | ) 38 | 39 | def setup_logger( 40 | name: Union[str, None] = None, 41 | log_file: str = "logs/app.log", 42 | level: Union[str, int] = "INFO", 43 | rotation: str = "10 MB", 44 | retention: str = "1 week", 45 | format: str = ( 46 | "[{level}] - " 47 | "{time:YYYY-MM-DD HH:mm:ss} - " 48 | "{name} - " 49 | "{message}" 50 | ), 51 | filter: Any = None 52 | ) -> logging.Logger: 53 | """ 54 | 全局初始化 loguru 日志记录器,并配置标准 logging 拦截到 loguru 中。 55 | 注意:全局初始化只应在入口处调用一次。 56 | """ 57 | # 清除所有已有 sink(仅用于全局初始化) 58 | loguru_logger.remove() 59 | 60 | # 添加控制台输出 61 | loguru_logger.add( 62 | sys.stdout, 63 | format=format, 64 | level=level, 65 | colorize=True, 66 | filter=filter 67 | ) 68 | 69 | # 添加文件输出 70 | file_path = Path(log_file) 71 | file_path.parent.mkdir(parents=True, exist_ok=True) 72 | loguru_logger.add( 73 | str(file_path), 74 | rotation=rotation, 75 | retention=retention, 76 | format=format, 77 | level=level, 78 | encoding="utf-8", 79 | filter=filter 80 | ) 81 | 82 | # 配置标准 logging 拦截到 loguru 83 | logging_logger = logging.getLogger(name) if name else logging.getLogger() 84 | logging_logger.handlers.clear() 85 | logging_logger.addHandler(InterceptHandler()) 86 | logging_logger.setLevel(level) 87 | 88 | return logging_logger 89 | 90 | def get_logger(name: str) -> LoggerProtocol: 91 | """ 92 | 获取绑定指定名称的 loguru 日志记录器,绑定 extra 字段,用于日志格式中显示模块名称 93 | """ 94 | return loguru_logger.bind(name=name) 95 | -------------------------------------------------------------------------------- /app/core/logger/logger_config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from app.core.logger.logger import setup_logger, InterceptHandler, get_logger, loguru_logger 3 | from app.core.config_manager import ConfigManager 4 | 5 | def configure_logging(): 6 | """ 7 | 配置全局日志系统 8 | """ 9 | # 获取配置 10 | config_manager = ConfigManager() 11 | log_file = config_manager.get("log.file_path", "logs/app.log") 12 | log_level = config_manager.get("log.level", "INFO") 13 | 14 | # 定义日志过滤器 15 | def log_filter(record): 16 | """过滤掉不需要的日志""" 17 | # 过滤掉 uvicorn.protocols.http.h11_impl 的日志 18 | if record["name"].startswith("uvicorn.protocols.http.h11_impl"): 19 | return False 20 | return True 21 | 22 | # 初始化全局日志(清除之前所有 sink) 23 | root_logger = setup_logger( 24 | name=None, # 根记录器 25 | log_file=log_file, 26 | level=log_level, 27 | format="[{level}] - {time:YYYY-MM-DD HH:mm:ss} - {name} - {message}", 28 | rotation="10 MB", 29 | retention="1 week", 30 | filter=log_filter # 添加过滤器 31 | ) 32 | 33 | # 配置需要统一处理的 logger 列表 34 | loggers = [ 35 | logging.getLogger(), # 根记录器 36 | logging.getLogger('fastapi'), 37 | logging.getLogger('uvicorn'), 38 | logging.getLogger('uvicorn.access'), 39 | logging.getLogger('uvicorn.error'), 40 | logging.getLogger('question_service'), 41 | logging.getLogger('middleware'), 42 | logging.getLogger('service'), 43 | ] 44 | 45 | for logger_obj in loggers: 46 | logger_obj.handlers = [] 47 | logger_obj.addHandler(InterceptHandler()) 48 | logger_obj.setLevel(log_level) 49 | logger_obj.propagate = False 50 | 51 | 52 | return root_logger 53 | -------------------------------------------------------------------------------- /app/core/security.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from fastapi import HTTPException, Security, Depends 3 | from fastapi.security import APIKeyHeader, HTTPBearer, HTTPAuthorizationCredentials 4 | from starlette.status import HTTP_403_FORBIDDEN 5 | from loguru import logger 6 | 7 | from .config_manager import ConfigManager 8 | 9 | # 创建认证处理器 10 | api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False) 11 | bearer_auth = HTTPBearer(auto_error=False) 12 | 13 | async def verify_api_key( 14 | api_key_header: Optional[str] = Security(api_key_header), 15 | bearer_auth: Optional[HTTPAuthorizationCredentials] = Security(bearer_auth), 16 | config: ConfigManager = Depends(lambda: ConfigManager()) 17 | ) -> None: 18 | """ 19 | 验证API Key的依赖函数,支持两种方式: 20 | 1. Authorization: Bearer 21 | 2. X-API-Key: 22 | 23 | Args: 24 | api_key_header: 从X-API-Key头获取的API Key 25 | bearer_auth: 从Authorization头获取的Bearer凭证 26 | config: 配置管理器实例 27 | 28 | Raises: 29 | HTTPException: 当API Key验证失败时抛出 30 | """ 31 | try: 32 | # 检查是否启用了API Key认证 33 | if not config.get("api.enable_api_key"): 34 | return 35 | 36 | # 获取API Key(优先使用Authorization header) 37 | api_key = None 38 | if bearer_auth: 39 | api_key = bearer_auth.credentials 40 | elif api_key_header: 41 | api_key = api_key_header 42 | 43 | if not api_key: 44 | raise HTTPException( 45 | status_code=HTTP_403_FORBIDDEN, 46 | detail="未提供API Key(支持Authorization: Bearer 或 X-API-Key: )" 47 | ) 48 | 49 | # 获取允许的API Keys列表 50 | allowed_keys = config.get("api.api_keys") 51 | if not isinstance(allowed_keys, list): 52 | logger.error("配置错误:api.api_keys 必须是一个列表") 53 | raise HTTPException( 54 | status_code=HTTP_403_FORBIDDEN, 55 | detail="API认证配置错误" 56 | ) 57 | 58 | # 验证API Key 59 | if api_key not in allowed_keys: 60 | logger.warning(f"无效的API Key尝试: {api_key[:8]}...") 61 | raise HTTPException( 62 | status_code=HTTP_403_FORBIDDEN, 63 | detail="无效的API Key" 64 | ) 65 | 66 | logger.debug(f"API Key验证成功: {api_key[:8]}...") 67 | 68 | except KeyError as e: 69 | logger.error(f"配置错误: {str(e)}") 70 | raise HTTPException( 71 | status_code=HTTP_403_FORBIDDEN, 72 | detail="API认证配置错误" 73 | ) -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | 通义千问API服务主程序 3 | """ 4 | import uvicorn 5 | from fastapi import FastAPI, Request 6 | from fastapi.middleware.cors import CORSMiddleware 7 | from fastapi.responses import JSONResponse 8 | 9 | from app.router.account import router as account_router 10 | from app.router.model import router as model_router 11 | from app.router.chat import router as chat_router 12 | from app.core.logger.logger import get_logger 13 | from app.core.config_manager import ConfigManager 14 | from app.core.account_manager import AccountManager 15 | from fastapi.staticfiles import StaticFiles 16 | logger = get_logger(__name__) 17 | config_manager = ConfigManager() 18 | account_manager = AccountManager() 19 | # 创建FastAPI实例 20 | app = FastAPI( 21 | title="通义千问 API", 22 | description="Python版通义千问API服务", 23 | version="1.0.0" 24 | ) 25 | 26 | # 添加CORS中间件 27 | app.add_middleware( 28 | CORSMiddleware, 29 | allow_origins=["*"], 30 | allow_credentials=True, 31 | allow_methods=["*"], 32 | allow_headers=["*"], 33 | ) 34 | 35 | # 注册API路由 36 | app.include_router(account_router) 37 | app.include_router(model_router) 38 | app.include_router(model_router, prefix="/v1") 39 | app.include_router(chat_router) 40 | app.mount("/static/", StaticFiles(directory="static",html=True), name="static") 41 | def get_start_info() -> str: 42 | """ 43 | 获取启动信息字符串 44 | 45 | Returns: 46 | str: 启动信息 47 | """ 48 | listen_address = config_manager.get("api.listen_address", "0.0.0.0") 49 | service_port = config_manager.get("api.port", 8000) 50 | api_prefix = "/v1" 51 | account_count = len(account_manager.get_enabled_accounts()) 52 | api_keys_count = len(config_manager.get("api.api_keys", [])) 53 | 54 | return f""" 55 | ------------------------------------------------------------------- 56 | 监听地址:{listen_address} 57 | 服务端口:{service_port} 58 | API前缀:{api_prefix} 59 | 账户数:{account_count} 60 | API密钥数:{api_keys_count} 61 | ------------------------------------------------------------------- 62 | """ -------------------------------------------------------------------------------- /app/models/account.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from typing import Dict, List, Optional 3 | 4 | class LoginRequest(BaseModel): 5 | """登录请求模型""" 6 | username: str = Field(..., description="用户名") 7 | password: str = Field(..., description="密码") 8 | 9 | class AccountResponse(BaseModel): 10 | """账号信息响应模型""" 11 | username: str = Field(..., description="用户名") 12 | enabled: bool = Field(..., description="是否启用") 13 | expires_at: Optional[int] = Field(None, description="过期时间戳") 14 | 15 | class AccountStatusUpdate(BaseModel): 16 | """账号状态更新请求模型""" 17 | enabled: bool = Field(..., description="是否启用") 18 | 19 | class CommonCookiesUpdate(BaseModel): 20 | """通用 cookies 更新请求模型""" 21 | cookies: Dict[str, str] = Field(..., description="Cookie 字典") 22 | 23 | class BaseResponse(BaseModel): 24 | """基础响应模型""" 25 | code: int = Field(200, description="状态码") 26 | message: str = Field("success", description="响应消息") 27 | data: Optional[dict] = Field(None, description="响应数据") -------------------------------------------------------------------------------- /app/models/chat.py: -------------------------------------------------------------------------------- 1 | """ 2 | API数据模型 3 | """ 4 | from typing import List, Dict, Any, Optional, Union 5 | from pydantic import BaseModel, Field 6 | 7 | 8 | class Message(BaseModel): 9 | """聊天消息模型""" 10 | role: str = Field(..., description="消息角色") 11 | content: Union[str, List[Dict[str, Any]]] = Field(..., description="消息内容") 12 | extra: Optional[Dict[str, Any]] = Field(None, description="额外信息") 13 | feature_config: Optional[Dict[str, Any]] = Field(None, description="特性配置") 14 | 15 | 16 | class ChatRequest(BaseModel): 17 | """聊天请求模型""" 18 | model: str = Field(..., description="模型名称") 19 | messages: List[Message] = Field(..., description="消息列表") 20 | stream: Optional[bool] = Field(None, description="是否使用流式响应") 21 | id: Optional[str] = Field(None, description="请求ID") 22 | temperature: Optional[float] = Field(None, description="采样温度,控制输出的随机性,取值范围0-2,值越大随机性越强") 23 | 24 | 25 | class ImageRequest(BaseModel): 26 | """图像生成请求模型""" 27 | model: str = Field(..., description="模型名称") 28 | prompt: str = Field(..., description="提示词") 29 | n: int = Field(1, description="生成数量") 30 | size: str = Field("1024*1024", description="图像尺寸") 31 | 32 | class VideoRequest(BaseModel): 33 | """视频生成请求模型""" 34 | model: str = Field(..., description="模型名称") 35 | prompt: str = Field(..., description="提示词") 36 | n: int = Field(1, description="生成数量") 37 | size: str = Field("1280x720", description="视频尺寸") -------------------------------------------------------------------------------- /app/models/model.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from typing import List, Dict, Any 3 | 4 | class ModelResponse(BaseModel): 5 | id: str 6 | object: str 7 | created: int 8 | owned_by: str 9 | 10 | class ModelList(BaseModel): 11 | object: str = "list" 12 | data: List[ModelResponse] 13 | -------------------------------------------------------------------------------- /app/router/account.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, HTTPException 2 | from typing import List 3 | 4 | from app.models.account import ( 5 | LoginRequest, 6 | AccountResponse, 7 | AccountStatusUpdate, 8 | CommonCookiesUpdate, 9 | BaseResponse 10 | ) 11 | from app.service.account_service import AccountService 12 | from app.core.account_manager import AccountManager 13 | from app.core.security import verify_api_key 14 | router = APIRouter(prefix="/accounts", tags=["accounts"]) 15 | 16 | 17 | account_service = AccountService() 18 | account_manager = AccountManager() 19 | @router.post("/login", response_model=BaseResponse) 20 | async def _login( 21 | request: LoginRequest, 22 | auth: AccountService = Depends(verify_api_key) 23 | ): 24 | """ 25 | 账号登录 26 | 27 | Args: 28 | request: 登录请求 29 | cookie: 登录成功后的 cookie 30 | expires_at: cookie 过期时间 31 | auth: 账号服务实例 32 | 33 | Returns: 34 | BaseResponse: 登录结果 35 | """ 36 | try: 37 | account = await account_service.login( 38 | username=request.username, 39 | password=request.password, 40 | ) 41 | return BaseResponse( 42 | message="登录成功", 43 | data=account 44 | ) 45 | except Exception as e: 46 | raise HTTPException(status_code=400, detail=str(e)) 47 | 48 | @router.post("/logout/{username}", response_model=BaseResponse) 49 | async def logout( 50 | username: str, 51 | auth: AccountService = Depends(verify_api_key) 52 | ): 53 | """ 54 | 账号登出 55 | 56 | Args: 57 | username: 用户名 58 | auth: 账号服务实例 59 | 60 | Returns: 61 | BaseResponse: 登出结果 62 | """ 63 | success = await account_service.logout(username) 64 | if not success: 65 | raise HTTPException(status_code=400, detail="登出失败") 66 | return BaseResponse(message="登出成功") 67 | 68 | @router.get("/list", response_model=List[AccountResponse]) 69 | async def get_accounts( 70 | auth: AccountService = Depends(verify_api_key) 71 | ): 72 | """ 73 | 获取账号列表 74 | 75 | Args: 76 | auth: 账号服务实例 77 | 78 | Returns: 79 | List[AccountResponse]: 账号列表 80 | """ 81 | return await account_service.get_accounts() 82 | 83 | @router.post("/{username}/status", response_model=BaseResponse) 84 | async def update_account_status( 85 | username: str, 86 | status: AccountStatusUpdate, 87 | auth: AccountService = Depends(verify_api_key) 88 | ): 89 | """ 90 | 更新账号状态 91 | 92 | Args: 93 | username: 用户名 94 | status: 状态更新请求 95 | auth: 账号服务实例 96 | 97 | Returns: 98 | BaseResponse: 更新结果 99 | """ 100 | success = await account_service.update_account_status(username, status.enabled) 101 | if not success: 102 | raise HTTPException(status_code=400, detail="状态更新失败") 103 | return BaseResponse(message="状态更新成功") 104 | @router.post("/{username}/refresh", response_model=BaseResponse) 105 | async def refresh_account( 106 | username: str, 107 | auth: AccountService = Depends(verify_api_key) 108 | ): 109 | """ 110 | 刷新账号 111 | 112 | Args: 113 | username: 用户名 114 | auth: 账号服务实例 115 | 116 | Returns: 117 | BaseResponse: 更新结果 118 | """ 119 | try: 120 | account = account_manager.get_account_by_username(username) 121 | success = await account_service.login(account['username'], account['password']) 122 | return BaseResponse(message="刷新成功") 123 | except Exception as e: 124 | raise HTTPException(status_code=400, detail=str(e)) 125 | @router.post("/common-cookies", response_model=BaseResponse) 126 | async def update_common_cookies( 127 | cookies: CommonCookiesUpdate, 128 | auth: AccountService = Depends(verify_api_key) 129 | ): 130 | """ 131 | 更新通用 cookies 132 | 133 | Args: 134 | cookies: cookies 更新请求 135 | auth: 账号服务实例 136 | 137 | Returns: 138 | BaseResponse: 更新结果 139 | """ 140 | await account_service.update_common_cookies(cookies.cookies) 141 | return BaseResponse(message="通用 cookies 更新成功") 142 | 143 | @router.get("/common-cookies", response_model=BaseResponse) 144 | async def get_common_cookies( 145 | auth: AccountService = Depends(verify_api_key) 146 | ): 147 | """ 148 | 获取通用 cookies 149 | 150 | Args: 151 | auth: 账号服务实例 152 | 153 | Returns: 154 | BaseResponse: 包含通用 cookies 的响应 155 | """ 156 | cookies = await account_service.get_common_cookies() 157 | return BaseResponse( 158 | message="获取成功", 159 | data={"cookies": cookies} 160 | ) -------------------------------------------------------------------------------- /app/router/chat.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request, Depends, HTTPException, APIRouter 2 | from fastapi.responses import JSONResponse, StreamingResponse 3 | from app.service.model_service import ModelService 4 | from app.service.completion_service import CompletionService 5 | from app.service.message_service import MessageService 6 | from app.core.cookie_service import CookieService 7 | from app.core.security import verify_api_key 8 | from app.core.account_manager import AccountManager 9 | from app.models.chat import ChatRequest 10 | from app.service.upload_service import UploadService 11 | # 请确保已提前实例化 ModelService、CompletionService、MessageService 12 | model_service = ModelService() 13 | completion_service = CompletionService() 14 | cookie_service = CookieService(AccountManager()) 15 | upload_service = UploadService() 16 | message_service = MessageService(model_service, completion_service, cookie_service, upload_service) 17 | 18 | router = APIRouter(prefix="/v1", tags=["chat"]) 19 | 20 | @router.post("/chat/completions") 21 | async def openai_compatible_chat( 22 | request: ChatRequest, 23 | auth: str = Depends(verify_api_key) 24 | ): 25 | try: 26 | token = cookie_service.get_auth_token() 27 | result = await message_service.chat(request, token) # 直接传 Pydantic 实例 28 | if hasattr(result, "body_iterator"): # 判断是否为 StreamingResponse 29 | return result 30 | return JSONResponse(result) 31 | except Exception as e: 32 | raise HTTPException(status_code=500, detail=str(e)) -------------------------------------------------------------------------------- /app/router/model.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, HTTPException 2 | from typing import List 3 | 4 | from app.service.account_service import AccountService 5 | from app.core.security import verify_api_key 6 | from app.service.model_service import ModelService 7 | from app.models.model import ModelResponse, ModelList 8 | router = APIRouter(prefix="/models", tags=["models"]) 9 | 10 | model_service = ModelService() 11 | 12 | @router.get("", response_model=ModelList) 13 | async def get_models( 14 | auth: AccountService = Depends(verify_api_key) 15 | ): 16 | """ 17 | 获取模型列表 18 | """ 19 | return await model_service.get_models() 20 | 21 | @router.post("/update", response_model=ModelList) 22 | async def update_models( 23 | auth: AccountService = Depends(verify_api_key) 24 | ): 25 | """ 26 | 更新模型列表 27 | """ 28 | await model_service.refresh_models() 29 | return await model_service.get_models() -------------------------------------------------------------------------------- /app/service/account_service.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import time 3 | import httpx 4 | from typing import Dict, List, Optional, Any 5 | from fastapi import HTTPException 6 | from app.core.account_manager import AccountManager 7 | from app.models.account import AccountResponse 8 | from app.core.cookie_service import CookieService 9 | class AccountService: 10 | def __init__(self): 11 | """初始化账号服务""" 12 | self.account_manager = AccountManager() 13 | self.cookie_service = CookieService(self.account_manager) 14 | 15 | def _sha256(self, text: str) -> str: 16 | """ 17 | 计算文本的SHA256哈希值 18 | 19 | Args: 20 | text: 要计算哈希的文本 21 | 22 | Returns: 23 | str: SHA256哈希值 24 | """ 25 | return hashlib.sha256(text.encode()).hexdigest() 26 | 27 | async def login(self, username: str, password: str) -> Dict: 28 | """ 29 | 账号登录 30 | 31 | Args: 32 | username: 用户名 33 | password: 密码 34 | 35 | Returns: 36 | Dict: 账号信息 37 | 38 | Raises: 39 | HTTPException: 登录失败时抛出 40 | """ 41 | try: 42 | # 计算密码的SHA256值 43 | hashed_password = self._sha256(password) 44 | 45 | # 获取请求头 46 | headers = self.cookie_service.get_headers() 47 | # 添加登录特定的请求头 48 | headers.update({ 49 | "x-request-id": f"{time.time()}-{hash(username)}", 50 | "Referer": "https://chat.qwen.ai/auth?action=signin", 51 | "bx-v": "2.5.28", 52 | "version": "0.0.57" 53 | }) 54 | 55 | data = { 56 | "email": username, 57 | "password": hashed_password 58 | } 59 | 60 | # 发送登录请求 61 | async with httpx.AsyncClient() as client: 62 | response = await client.post( 63 | "https://chat.qwen.ai/api/v1/auths/signin", 64 | headers=headers, 65 | json=data, 66 | timeout=30.0 67 | ) 68 | 69 | if response.status_code != 200: 70 | raise HTTPException(status_code=response.status_code, detail=response.text) 71 | 72 | result = response.json() 73 | cookie = response.headers.get('set-cookie', '') 74 | expires_at = result.get('expires_at', 0) 75 | 76 | try: 77 | # 创建基础账号信息 78 | account = self.account_manager.add_account(username, password) 79 | 80 | # 完成账号信息添加 81 | if not self.account_manager.complete_account_info(username, cookie, expires_at): 82 | raise HTTPException(status_code=400, detail="账号信息添加失败") 83 | 84 | return account 85 | except ValueError: 86 | # 账号已存在,更新信息 87 | updates = { 88 | "cookie": cookie, 89 | "token": result.get('token', ''), 90 | "expires_at": expires_at 91 | } 92 | if not self.account_manager.update_account(username, updates): 93 | raise HTTPException(status_code=400, detail="账号信息更新失败") 94 | 95 | return self.account_manager.get_account_by_username(username) 96 | 97 | except Exception as e: 98 | raise HTTPException(status_code=500, detail=str(e)) 99 | 100 | async def logout(self, username: str) -> bool: 101 | """ 102 | 账号登出 103 | 104 | Args: 105 | username: 用户名 106 | 107 | Returns: 108 | bool: 是否成功登出 109 | 110 | Raises: 111 | HTTPException: 账号不存在时抛出 112 | """ 113 | account = self.account_manager.get_account_by_username(username) 114 | if not account: 115 | raise HTTPException(status_code=404, detail="账号不存在") 116 | 117 | return self.account_manager.delete_account(username) 118 | 119 | async def get_accounts(self) -> List[AccountResponse]: 120 | """ 121 | 获取所有账号列表 122 | 123 | Returns: 124 | List[AccountResponse]: 账号列表 125 | """ 126 | accounts = self.account_manager.get_all_accounts() 127 | return [ 128 | AccountResponse( 129 | username=account["username"], 130 | enabled=account["enabled"], 131 | expires_at=account.get("expires_at") 132 | ) 133 | for account in accounts 134 | ] 135 | 136 | async def update_account_status(self, username: str, enabled: bool) -> bool: 137 | """ 138 | 更新账号状态 139 | 140 | Args: 141 | username: 用户名 142 | enabled: 是否启用 143 | 144 | Returns: 145 | bool: 是否更新成功 146 | 147 | Raises: 148 | HTTPException: 账号不存在时抛出 149 | """ 150 | if not self.account_manager.get_account_by_username(username): 151 | raise HTTPException(status_code=404, detail="账号不存在") 152 | 153 | return self.account_manager.update_account(username, {"enabled": enabled}) 154 | 155 | async def update_common_cookies(self, cookies: Dict[str, str]) -> None: 156 | """ 157 | 更新通用 cookies 158 | 159 | Args: 160 | cookies: 新的 cookies 字典 161 | """ 162 | self.account_manager.update_common_cookies(cookies) 163 | 164 | async def get_common_cookies(self) -> Dict[str, str]: 165 | """ 166 | 获取通用 cookies 167 | 168 | Returns: 169 | Dict[str, str]: 通用 cookies 字典 170 | """ 171 | return self.account_manager.get_common_cookies() -------------------------------------------------------------------------------- /app/service/message_service.py: -------------------------------------------------------------------------------- 1 | # app/service/message_service.py 2 | 3 | import json 4 | import asyncio 5 | from typing import Dict, List, Any, AsyncGenerator 6 | from app.models.chat import ChatRequest 7 | from app.service.completion_service import CompletionService 8 | from app.service.model_service import ModelService 9 | from app.service.task_service import TaskService 10 | from app.core.cookie_service import CookieService 11 | from fastapi.responses import StreamingResponse 12 | from app.core.logger.logger import get_logger 13 | 14 | # 新增导入 15 | from app.service.upload_service import UploadService 16 | 17 | logger = get_logger(__name__) 18 | 19 | 20 | # -- 新增基础处理函数 -- 21 | async def process_user_images(msgs: list, auth_token: str, upload_service: UploadService): 22 | """ 23 | 将user消息中的base64类型图片上传OSS,替换成合法图片url 24 | """ 25 | for msg in msgs: 26 | if msg.get("role") != "user": 27 | continue 28 | content = msg.get("content") 29 | if not isinstance(content, list): 30 | continue 31 | new_content = [] 32 | for item in content: 33 | if item.get("type") == "image_url": 34 | image_url = item.get("image_url", {}).get("url", "") 35 | if image_url.startswith("data:image/"): # base64 格式 36 | try: 37 | img_url = await upload_service.save_url(image_url, auth_token) 38 | if img_url: 39 | new_content.append({"type": "image", "image": img_url}) 40 | continue # 跳过原item 41 | except Exception as e: 42 | logger.warning(f"Base64图片上传失败:{e}") 43 | new_content.append(item) 44 | msg["content"] = new_content 45 | 46 | 47 | class MessageService: 48 | def __init__( 49 | self, 50 | model_service: ModelService, 51 | completion_service: CompletionService, 52 | cookie_service: CookieService, 53 | upload_service: UploadService, # 新增 54 | ): 55 | self.model_service = model_service 56 | self.completion_service = completion_service 57 | self.cookie_service = cookie_service 58 | self.task_service = TaskService(cookie_service) 59 | self.upload_service = upload_service # 新增 60 | 61 | async def chat( 62 | self, 63 | client_payload: ChatRequest, 64 | auth_token: str 65 | ): 66 | model = client_payload.model 67 | messages = [msg.dict() for msg in client_payload.messages] 68 | 69 | # ========== 新增处理:base64 image_url替换 ========== 70 | await process_user_images(messages, auth_token, self.upload_service) 71 | # ================================================== 72 | 73 | temperature = client_payload.temperature if client_payload.temperature is not None else 1.0 74 | stream = client_payload.stream if client_payload.stream is not None else False 75 | 76 | # 保存客户端原始stream请求标志 77 | original_stream_request = stream 78 | 79 | model_config = self.model_service.get_model_config(model) 80 | chat_type = model_config["completion"].get("chat_type", "t2t") 81 | sub_chat_type = model_config["completion"].get("sub_chat_type", "t2t") 82 | chat_mode = model_config["completion"].get("chat_mode", "normal") 83 | feature_config = model_config["message"].get("feature_config", {}) 84 | message_chat_type = model_config["message"].get("chat_type", "normal") 85 | task_type = await self.model_service.get_task_type(model) 86 | size = model_config["completion"].get("size") 87 | # 对于t2i和t2v任务,强制使用非流式响应 88 | if task_type in ('t2i', 't2v'): 89 | stream = False 90 | 91 | # 处理所有消息,确保字段正确 92 | qwen_messages = [] 93 | for m in messages: 94 | m2 = dict(m) 95 | # 设置正确的chat_type 96 | m2["chat_type"] = message_chat_type 97 | # 确保extra字段存在且不为null 98 | m2["extra"] = {} if m2.get("extra") is None else m2.get("extra", {}) 99 | # 确保feature_config字段存在且不为null,对于t2i任务强制设置thinking_enabled为false 100 | if task_type == 't2i': 101 | m2["feature_config"] = { 102 | "thinking_enabled": False, 103 | "output_schema": "phase" 104 | } 105 | else: 106 | # 修复:当feature_config为None时使用默认值 107 | m2["feature_config"] = feature_config if m2.get("feature_config") is None else m2.get("feature_config") 108 | #logger.info(f"m2: {m2}") 109 | qwen_messages.append(m2) 110 | 111 | real_model = await self.model_service.get_real_model(model) 112 | #logger.info(f"qwen_messages: {qwen_messages}") 113 | if stream: 114 | stream_gen = self.completion_service.stream_completion( 115 | messages=qwen_messages, 116 | auth_token=auth_token, 117 | model=real_model, 118 | stream=stream, 119 | chat_type=chat_type, 120 | sub_chat_type=sub_chat_type, 121 | chat_mode=chat_mode, 122 | temperature=temperature, 123 | size=size 124 | ) 125 | return StreamingResponse(stream_gen, media_type="text/event-stream") 126 | else: 127 | #logger.info(f"非流式请求") 128 | result, response_data = await self.completion_service.chat_completion( 129 | messages=qwen_messages, 130 | auth_token=auth_token, 131 | model=real_model, 132 | stream=stream, 133 | chat_type=chat_type, 134 | sub_chat_type=sub_chat_type, 135 | chat_mode=chat_mode, 136 | temperature=temperature, 137 | size=size 138 | ) 139 | #print(f"result: {result}") 140 | #print(f"response_data: {response_data}") 141 | 142 | # 处理任务型响应(t2i和t2v) 143 | task_result = None 144 | if task_type in ('t2i', 't2v'): 145 | task_id = self._extract_task_id(response_data) 146 | if not task_id: 147 | return { 148 | "chat_type": task_type, 149 | "task_status": "failed", 150 | "message": "未能获取任务ID", 151 | "remaining_time": "", 152 | "content": "" 153 | } 154 | 155 | # 根据任务类型选择合适的轮询方法 156 | if task_type == 't2i': 157 | task_result = await self.task_service.poll_image_task( 158 | task_id=task_id, 159 | auth_token=auth_token 160 | ) 161 | else: # t2v 162 | task_result = await self.task_service.poll_video_task( 163 | task_id=task_id, 164 | auth_token=auth_token 165 | ) 166 | #logger.info(f"task_result: {task_result}") 167 | 168 | # 根据客户端原始请求类型,选择合适的响应格式 169 | formatted_result = self._format_sync_response(task_result) 170 | if original_stream_request: 171 | # 如果客户端请求流式响应,将结果转换为流式格式返回 172 | stream_gen = self._convert_to_streaming_response(formatted_result) 173 | return StreamingResponse(stream_gen, media_type="text/event-stream") 174 | else: 175 | # 如果客户端请求非流式响应,直接返回结果 176 | return formatted_result 177 | else: 178 | # 对于普通文本对话,使用 format_sync_response 处理思考模式 179 | return self._format_sync_response(result) 180 | 181 | def _extract_task_id(self, response: Dict[str, Any]) -> str: 182 | """ 183 | 从响应中提取任务ID 184 | 185 | Args: 186 | response: 完整的响应数据 187 | 188 | Returns: 189 | str: 任务ID,如果未找到则返回空字符串 190 | """ 191 | try: 192 | messages = response.get("messages", []) 193 | if not messages: 194 | return "" 195 | 196 | # 获取最后一条消息 197 | last_message = messages[-1] 198 | task_id = last_message.get("extra", {}).get("wanx", {}).get("task_id") 199 | if task_id: 200 | return task_id 201 | except Exception: 202 | pass 203 | return "" 204 | 205 | def _format_sync_response(self, qwen_response: dict): 206 | if not qwen_response or "choices" not in qwen_response: 207 | #logger.info(f"qwen_response: {qwen_response}") 208 | return qwen_response 209 | choices = qwen_response["choices"] 210 | think_idx = [i for i, c in enumerate(choices) 211 | if c.get("message", {}).get("phase") == "think"] 212 | if not think_idx: 213 | #logger.info(f"qwen_response: {qwen_response}") 214 | return qwen_response 215 | for i, idx in enumerate(think_idx): 216 | content = choices[idx]["message"]["content"] 217 | if i == 0: 218 | content = f"{content}" 219 | if i == len(think_idx) - 1: 220 | content = f"{content}" 221 | choices[idx]["message"]["content"] = content 222 | choices[idx]["delta"]["reasoning_content"] = choices[idx]["message"]["content"].replace("", "").replace("", "") 223 | #logger.info(f"qwen_response: {qwen_response}") 224 | return qwen_response 225 | 226 | async def _convert_to_streaming_response(self, response_data: dict) -> AsyncGenerator[bytes, None]: 227 | """ 228 | 将非流式响应转换为流式响应格式 229 | 230 | Args: 231 | response_data: 原始的非流式响应数据 232 | 233 | Returns: 234 | AsyncGenerator: 流式响应生成器 235 | """ 236 | try: 237 | # 检查响应数据是否有效 238 | if not response_data or "choices" not in response_data: 239 | yield f"data: {json.dumps({'error': '无效的响应数据'})}\n\n".encode("utf-8") 240 | yield b"data: [DONE]\n\n" 241 | return 242 | 243 | # 获取响应内容 244 | choices = response_data.get("choices", []) 245 | if not choices: 246 | yield f"data: {json.dumps({'error': '响应中没有内容'})}\n\n".encode("utf-8") 247 | yield b"data: [DONE]\n\n" 248 | return 249 | 250 | # 获取第一个选择项的消息内容 251 | message = choices[0].get("message", {}) 252 | content = message.get("content", "") 253 | role = message.get("role", "assistant") 254 | 255 | # 构建流式响应数据 256 | chunk = { 257 | "choices": [{ 258 | "index": 0, 259 | "delta": { 260 | "role": role, 261 | "content": content 262 | }, 263 | "finish_reason": "stop" 264 | }] 265 | } 266 | 267 | # 发送流式响应 268 | yield f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n".encode("utf-8") 269 | 270 | # 发送完成标记 271 | await asyncio.sleep(0.1) # 短暂延迟,确保客户端能够正确接收 272 | yield b"data: [DONE]\n\n" 273 | 274 | except Exception as e: 275 | logger.error(f"转换流式响应时出错: {str(e)}") 276 | yield f"data: {json.dumps({'error': str(e)})}\n\n".encode("utf-8") 277 | yield b"data: [DONE]\n\n" -------------------------------------------------------------------------------- /app/service/model_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | 模型服务 3 | """ 4 | from typing import Dict, Any, List, Optional 5 | import json 6 | from pathlib import Path 7 | import httpx 8 | from app.core.logger.logger import get_logger 9 | from app.core.cookie_service import CookieService 10 | from app.core.account_manager import AccountManager 11 | from app.core.config_manager import ConfigManager 12 | config_manager = ConfigManager() 13 | 14 | logger = get_logger(__name__) 15 | 16 | class ModelServiceError(Exception): 17 | """模型服务相关错误""" 18 | pass 19 | 20 | class ModelService: 21 | """模型服务""" 22 | 23 | # 模型功能后缀 24 | MODEL_FEATURES = [ 25 | '', # 基础模型 26 | '-thinking', # 思考模式 27 | '-search', # 搜索模式 28 | '-thinking-search', # 思考+搜索模式 29 | '-draw', # 绘图模式 30 | '-video' # 视频生成模式 31 | ] 32 | 33 | # 模型配置映射 34 | MODEL_CONFIGS = { 35 | 'base': { # 基础模型配置 36 | 'completion': { 37 | 'chat_type': 't2t', 38 | 'sub_chat_type': 't2t', 39 | 'chat_mode': 'normal', 40 | }, 41 | 'message': { 42 | 'feature_config': { 43 | 'thinking_enabled': False, 44 | 'output_schema': 'phase', 45 | }} 46 | }, 47 | 'thinking': { # 思考模式配置 48 | 'completion': { 49 | 'chat_type': 't2t', 50 | 'sub_chat_type': 't2t', 51 | 'chat_mode': 'normal', 52 | }, 53 | 'message': { 54 | 'feature_config': { 55 | 'thinking_enabled': True, 56 | 'output_schema': 'phase', 57 | 'thinking_budget': 38912 58 | }} 59 | }, 60 | 'search': { # 搜索模式配置 61 | 'completion': { 62 | 'chat_type': 't2t', 63 | 'sub_chat_type': 't2t', 64 | 'chat_mode': 'normal', 65 | }, 66 | 'message': { 67 | 'chat_type': 'search', 68 | 'feature_config': { 69 | 'thinking_enabled': False, 70 | 'output_schema': 'phase', 71 | }} 72 | }, 73 | 'thinking-search': { # 思考+搜索模式配置 74 | 'completion': { 75 | 'chat_type': 't2t', 76 | 'sub_chat_type': 't2t', 77 | 'chat_mode': 'normal', 78 | }, 79 | 'message': { 80 | 'chat_type': 'search', 81 | 'feature_config': { 82 | 'thinking_enabled': True, 83 | 'output_schema': 'phase', 84 | 'thinking_budget': 38912 85 | }} 86 | }, 87 | 'draw': { # 绘图模式配置 88 | 'completion': { 89 | 'chat_type': 't2i', 90 | 'sub_chat_type': 't2i', 91 | 'chat_mode': 'normal', 92 | 'stream': False, 93 | 'size': config_manager.get("image.size", "1:1") 94 | }, 95 | 'message': { 96 | 'chat_type': 't2i', 97 | 'feature_config': { 98 | 'thinking_enabled': False, 99 | 'output_schema': 'phase' 100 | }} 101 | }, 102 | 'video': { # 视频生成模式配置 103 | 'completion': { 104 | 'chat_type': 't2v', 105 | 'sub_chat_type': 't2v', 106 | 'chat_mode': 'normal', 107 | 'stream': False, 108 | 'size': config_manager.get("video.size", "1280x720") 109 | }, 110 | 'message': { 111 | 'chat_type': 't2v', 112 | 'feature_config': { 113 | 'thinking_enabled': False, 114 | 'output_schema': 'phase' 115 | } 116 | }} 117 | # 还有artifacts,不过没什么用就不加了 118 | } 119 | 120 | def __init__(self): 121 | """初始化模型服务""" 122 | 123 | 124 | self.model_file = Path("data/model.json") 125 | self._models: List[Dict[str, Any]] = [] 126 | self.account_manager = AccountManager() 127 | self.cookie_service = CookieService(self.account_manager) 128 | self.config_manager = ConfigManager() 129 | self.base_url = self.config_manager.get("api.url", "https://chat.qwen.ai/api") 130 | self._load_models_from_file() 131 | 132 | def _convert_to_openai_format(self, model_id: str) -> List[Dict[str, Any]]: 133 | """ 134 | 将通义千问模型ID转换为OpenAI格式,并添加功能后缀 135 | 136 | Args: 137 | model_id: 通义千问模型ID 138 | 139 | Returns: 140 | List[Dict[str, Any]]: OpenAI格式的模型信息列表 141 | """ 142 | return [{ 143 | "id": f"{model_id}{suffix}", 144 | "object": "model", 145 | "created": 0, 146 | "owned_by": "qwen" 147 | } for suffix in self.MODEL_FEATURES] 148 | 149 | def _load_models_from_file(self) -> None: 150 | """从model.json文件加载模型列表,如果文件不存在或加载失败则从API获取""" 151 | try: 152 | if self.model_file.exists(): 153 | data = json.loads(self.model_file.read_text(encoding='utf-8')) 154 | self._models = data.get('data', []) 155 | if self._models: 156 | return 157 | self._fetch_and_save_models() 158 | except Exception as e: 159 | logger.error(f"加载模型列表失败: {str(e)}") 160 | self._fetch_and_save_models() 161 | 162 | async def _fetch_and_save_models(self) -> None: 163 | """从API获取模型列表并保存到文件""" 164 | try: 165 | auth_token = self.cookie_service.get_auth_token() 166 | headers = self.cookie_service.get_headers(auth_token) 167 | 168 | async with httpx.AsyncClient() as client: 169 | response = await client.get( 170 | f"{self.base_url}/models", 171 | headers=headers, 172 | timeout=30.0 173 | ) 174 | 175 | if response.status_code == 200: 176 | models_data = response.json() 177 | if not models_data or 'data' not in models_data: 178 | raise ModelServiceError("API返回的模型数据格式错误") 179 | 180 | self._models = [] 181 | for item in models_data['data']: 182 | if model_id := item.get('id'): 183 | self._models.extend(self._convert_to_openai_format(model_id)) 184 | self._save_models_to_file() 185 | return 186 | 187 | raise ModelServiceError(f"API请求失败: {response.status_code}") 188 | 189 | except Exception as e: 190 | logger.error(f"从API获取模型列表失败: {str(e)}") 191 | self._models = [] 192 | 193 | def _save_models_to_file(self) -> None: 194 | """将当前模型列表保存到文件""" 195 | try: 196 | self.model_file.write_text( 197 | json.dumps({ 198 | "object": "list", 199 | "data": self._models 200 | }, ensure_ascii=False, indent=2), 201 | encoding='utf-8' 202 | ) 203 | except Exception as e: 204 | logger.error(f"保存模型列表失败: {str(e)}") 205 | 206 | def set_models(self, models: List[str]) -> None: 207 | """ 208 | 设置模型列表并保存到文件 209 | 210 | Args: 211 | models: 新的模型列表 212 | """ 213 | self._models = [] 214 | for model in models: 215 | self._models.extend(self._convert_to_openai_format(model)) 216 | self._save_models_to_file() 217 | 218 | async def get_models(self) -> Dict[str, Any]: 219 | """ 220 | 获取可用模型列表,优先使用缓存,失败时从API获取 221 | 222 | Returns: 223 | Dict[str, Any]: 包含object和data字段的模型列表 224 | """ 225 | if self._models: 226 | return { 227 | "object": "list", 228 | "data": self._models 229 | } 230 | 231 | await self._fetch_and_save_models() 232 | return { 233 | "object": "list", 234 | "data": self._models 235 | } 236 | async def refresh_models(self) -> None: 237 | """刷新模型列表""" 238 | await self._fetch_and_save_models() 239 | def get_completion_config(self, model: str) -> Dict[str, Any]: 240 | """ 241 | 获取模型的completion service配置参数 242 | 243 | Args: 244 | model: 模型名称 245 | 246 | Returns: 247 | Dict[str, Any]: completion service配置参数 248 | """ 249 | # 提取特性后缀 250 | feature = 'base' 251 | for suffix in self.MODEL_FEATURES: 252 | if suffix and model.endswith(suffix): 253 | feature = suffix.lstrip('-') 254 | break 255 | 256 | # 获取配置 257 | config = self.MODEL_CONFIGS.get(feature, {}).get('completion', {}) 258 | if not config: 259 | # 如果没有找到配置,使用基础配置 260 | config = self.MODEL_CONFIGS['base']['completion'] 261 | 262 | return config 263 | 264 | def get_message_feature_config(self, model: str) -> Dict[str, Any]: 265 | """ 266 | 获取模型的message特性配置 267 | 268 | Args: 269 | model: 模型名称 270 | 271 | Returns: 272 | Dict[str, Any]: message特性配置 273 | """ 274 | # 提取特性后缀 275 | feature = 'base' 276 | for suffix in self.MODEL_FEATURES: 277 | if suffix and model.endswith(suffix): 278 | feature = suffix.lstrip('-') 279 | break 280 | 281 | # 获取配置 282 | config = self.MODEL_CONFIGS.get(feature, {}).get('message', {}) 283 | if not config: 284 | # 如果没有找到配置,使用基础配置 285 | config = self.MODEL_CONFIGS['base']['message'] 286 | 287 | return config 288 | 289 | def get_model_config(self, model: str) -> Dict[str, Any]: 290 | """ 291 | 获取模型的完整配置(包括completion和message配置) 292 | 293 | Args: 294 | model: 模型名称 295 | 296 | Returns: 297 | Dict[str, Any]: 完整配置 298 | """ 299 | completion_config = self.get_completion_config(model) 300 | message_config = self.get_message_feature_config(model) 301 | 302 | return { 303 | 'completion': completion_config, 304 | 'message': message_config 305 | } 306 | async def get_task_type(self, model_id: str) -> str: 307 | """ 308 | 获取任务类型 309 | """ 310 | if model_id.endswith('-draw'): 311 | return 't2i' 312 | elif model_id.endswith('-video'): 313 | return 't2v' 314 | else: 315 | return 't2t' 316 | async def get_real_model(self, model: str) -> str: 317 | """ 318 | 获取实际的模型名称 319 | 320 | Args: 321 | model: 模型名称 322 | 323 | Returns: 324 | str: 实际的模型名称 325 | """ 326 | # 获取基础模型(去除所有后缀) 327 | base_model = model 328 | for suffix in self.MODEL_FEATURES: 329 | if suffix: # 跳过空字符串 330 | base_model = base_model.replace(suffix, '') 331 | 332 | # 验证基础模型是否存在 333 | models_data = await self.get_models() 334 | model_ids = [m['id'] for m in models_data.get('data', [])] 335 | 336 | if base_model not in model_ids: 337 | logger.warning(f"模型 {model} 不在支持列表中,降级到默认模型 qwen-max-latest") 338 | return "qwen-max-latest" 339 | 340 | return base_model 341 | 342 | 343 | async def verify_model_with_feature(self, model: str) -> str: 344 | """ 345 | 验证模型是否支持特定功能,如果不支持则返回默认模型 346 | 347 | Args: 348 | model: 要验证的模型名 349 | 350 | Returns: 351 | str: 有效的模型名 352 | """ 353 | # 获取基础模型(去除所有后缀) 354 | base_model = model 355 | for suffix in self.MODEL_FEATURES: 356 | if suffix: # 跳过空字符串 357 | base_model = base_model.replace(suffix, '') 358 | 359 | # 验证基础模型是否存在 360 | models_data = await self.get_models() 361 | model_ids = [m['id'] for m in models_data.get('data', [])] 362 | 363 | if model not in model_ids: 364 | logger.warning(f"模型 {model} 不在支持列表中,降级到默认模型 qwen-turbo") 365 | return "qwen-turbo" 366 | 367 | return model -------------------------------------------------------------------------------- /app/service/task_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | 任务服务 3 | """ 4 | from typing import Dict, Any, Optional 5 | import asyncio 6 | import json 7 | from app.core.logger.logger import get_logger 8 | from app.core.cookie_service import CookieService 9 | import httpx 10 | import time 11 | from app.core.config_manager import ConfigManager 12 | import uuid 13 | config_manager = ConfigManager() 14 | logger = get_logger(__name__) 15 | 16 | class TaskService: 17 | """任务服务,处理异步任务状态查询""" 18 | 19 | def __init__(self, cookie_service: CookieService): 20 | """ 21 | 初始化任务服务 22 | 23 | Args: 24 | cookie_service: CookieService实例,用于获取认证信息 25 | """ 26 | self.cookie_service = cookie_service 27 | self.base_url = config_manager.get("api.url","https://chat.qwen.ai/api") 28 | 29 | async def poll_image_task( 30 | self, 31 | task_id: str, 32 | auth_token: str, 33 | max_retries: int = 60, 34 | retry_interval: float = 3.0, 35 | timeout: float = 180.0 36 | ) -> Dict[str, Any]: 37 | """ 38 | 轮询图片生成任务状态 39 | 40 | Args: 41 | task_id: 任务ID 42 | auth_token: 认证Token 43 | max_retries: 最大重试次数 44 | retry_interval: 重试间隔(秒) 45 | timeout: 超时时间(秒) 46 | 47 | Returns: 48 | Dict[str, Any]: 任务状态和结果 49 | """ 50 | return await self._poll_task( 51 | task_id=task_id, 52 | auth_token=auth_token, 53 | task_type="t2i", 54 | max_retries=max_retries, 55 | retry_interval=retry_interval, 56 | timeout=timeout 57 | ) 58 | 59 | async def poll_video_task( 60 | self, 61 | task_id: str, 62 | auth_token: str, 63 | max_retries: int = 120, 64 | retry_interval: float = 5.0, 65 | timeout: float = 600.0 66 | ) -> Dict[str, Any]: 67 | """ 68 | 轮询视频生成任务状态 69 | 70 | Args: 71 | task_id: 任务ID 72 | auth_token: 认证Token 73 | max_retries: 最大重试次数 74 | retry_interval: 重试间隔(秒) 75 | timeout: 超时时间(秒) 76 | 77 | Returns: 78 | Dict[str, Any]: 任务状态和结果 79 | """ 80 | return await self._poll_task( 81 | task_id=task_id, 82 | auth_token=auth_token, 83 | task_type="t2v", 84 | max_retries=max_retries, 85 | retry_interval=retry_interval, 86 | timeout=timeout 87 | ) 88 | 89 | async def _poll_task( 90 | self, 91 | task_id: str, 92 | auth_token: str, 93 | task_type: str, 94 | max_retries: int, 95 | retry_interval: float, 96 | timeout: float 97 | ) -> Dict[str, Any]: 98 | """ 99 | 通用任务轮询实现 100 | 101 | Args: 102 | task_id: 任务ID 103 | auth_token: 认证Token 104 | task_type: 任务类型(t2i或t2v) 105 | max_retries: 最大重试次数 106 | retry_interval: 重试间隔(秒) 107 | timeout: 超时时间(秒) 108 | 109 | Returns: 110 | Dict[str, Any]: 任务状态和结果 111 | """ 112 | 113 | 114 | start_time = time.time() 115 | retry_count = 0 116 | 117 | while retry_count < max_retries: 118 | try: 119 | status = await self.get_task_status(task_id, auth_token) 120 | logger.info(f"第{retry_count + 1}次检查任务状态: {json.dumps(status, ensure_ascii=False)}") 121 | 122 | # 检查任务是否完成 123 | task_status = status.get("task_status", "") 124 | 125 | # 任务失败 126 | if task_status == "failed": 127 | error_message = status.get("message", "未知错误") 128 | logger.error(f"任务失败: {error_message}") 129 | return self.format_task_response( 130 | task_type=task_type, 131 | status="failed", 132 | message=error_message 133 | ) 134 | 135 | # 任务成功 136 | if status.get("content"): 137 | logger.info("任务完成") 138 | return self.format_task_response( 139 | task_type=task_type, 140 | status="success", 141 | content=status["content"] 142 | ) 143 | 144 | # 检查是否超时 145 | if time.time() - start_time > timeout: 146 | logger.error("任务超时") 147 | return self.format_task_response( 148 | task_type=task_type, 149 | status="timeout", 150 | message="任务超时" 151 | ) 152 | 153 | # 继续等待 154 | await asyncio.sleep(retry_interval) 155 | retry_count += 1 156 | 157 | except Exception as e: 158 | logger.error(f"查询任务状态出错: {str(e)}") 159 | await asyncio.sleep(retry_interval) 160 | retry_count += 1 161 | 162 | # 达到最大重试次数 163 | return self.format_task_response( 164 | task_type=task_type, 165 | status="max_retries_exceeded", 166 | message="达到最大重试次数" 167 | ) 168 | 169 | async def get_task_status(self, task_id: str, auth_token: str) -> Dict[str, Any]: 170 | """ 171 | 获取任务状态 172 | 173 | Args: 174 | task_id: 任务ID 175 | auth_token: 认证Token 176 | 177 | Returns: 178 | Dict[str, Any]: 任务状态信息 179 | """ 180 | import httpx 181 | 182 | headers = self.cookie_service.get_headers(auth_token) 183 | 184 | async with httpx.AsyncClient() as client: 185 | response = await client.get( 186 | f"{self.base_url}/v1/tasks/status/{task_id}", 187 | headers=headers, 188 | timeout=30.0 189 | ) 190 | 191 | if response.status_code != 200: 192 | raise Exception(f"获取任务状态失败: {response.text}") 193 | 194 | return response.json() 195 | 196 | def format_task_response( 197 | self, 198 | task_type: str, 199 | status: str, 200 | message: str = "", 201 | content: str = "" 202 | ) -> Dict[str, Any]: 203 | """ 204 | 格式化任务响应 205 | 206 | Args: 207 | task_type: 任务类型(t2i或t2v) 208 | status: 任务状态 209 | message: 状态消息 210 | content: 任务结果内容 211 | 212 | Returns: 213 | Dict[str, Any]: 格式化的响应 214 | """ 215 | response = { 216 | "chat_type": task_type, 217 | "task_status": status, 218 | "message": message, 219 | "remaining_time": "", 220 | "content": content 221 | } 222 | 223 | # 如果是成功的图片任务,返回markdown格式 224 | if status == "success" and task_type == "t2i" and content: 225 | # 生成带横线的UUID 226 | uid = str(uuid.uuid4()) 227 | return { 228 | 'id': f'chatcmpl-{uid}', 229 | 'object': 'chat.completion', 230 | 'created': int(time.time()*1000), 231 | 'model': 'qwen-turbo', 232 | 'choices': [ 233 | { 234 | 'index': 0, 235 | 'message': { 236 | 'role': 'assistant', 237 | 'content': f"![Generated Image]({content})" 238 | }, 239 | 'finish_reason': 'stop' 240 | } 241 | ], 242 | 'usage': { 243 | 'prompt_tokens': len(content), # 使用content长度作为prompt_tokens 244 | 'completion_tokens': len(f"![Generated Image]({content})"), # 使用生成的markdown长度作为completion_tokens 245 | 'total_tokens': len(content) + len(f"![Generated Image]({content})") # 总token数 246 | } 247 | } 248 | elif status == "success" and task_type == "t2v" and content: 249 | uid = str(uuid.uuid4()) 250 | return { 251 | 'id': f'chatcmpl-{uid}', 252 | 'object': 'chat.completion', 253 | 'created': int(time.time()*1000), 254 | 'model': 'qwen-turbo', 255 | 'choices': [ 256 | { 257 | 'index': 0, 258 | 'message': { 259 | 'role': 'assistant', 260 | 'content': f"[视频链接]({content})" 261 | }, 262 | 'finish_reason': 'stop' 263 | } 264 | ], 265 | 'usage': {'prompt_tokens': 0, 'completion_tokens': 0, 'total_tokens': 0} 266 | } 267 | 268 | return response -------------------------------------------------------------------------------- /app/service/upload_service.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | import uuid 3 | import httpx 4 | import json 5 | import traceback 6 | import alibabacloud_oss_v2 as oss 7 | import base64 8 | from hmac import HMAC 9 | from hashlib import sha256 10 | import aiofiles 11 | import hashlib 12 | import asyncio 13 | import os 14 | from app.core.config_manager import ConfigManager 15 | from app.core.account_manager import AccountManager 16 | from app.core.cookie_service import CookieService 17 | from app.core.logger.logger import get_logger 18 | from app.service.account_service import AccountService 19 | config_manager = ConfigManager() 20 | account_manager = AccountManager() 21 | cookie_service = CookieService(account_manager) 22 | account_service = AccountService() 23 | logger = get_logger(__name__) 24 | 25 | UPLOAD_CACHE_FILE = os.path.join('data', 'upload.json') 26 | 27 | class UploadService: 28 | """ 29 | 上传服务,不依赖initialize,缓存操作fire&forget,主流程100%不会阻塞/卡死/报协程警告 30 | """ 31 | 32 | def __init__(self): 33 | self.upload_cache = {} 34 | self.cache_loaded = False 35 | self.cache_lock = asyncio.Lock() 36 | self._load_cache_launched = False 37 | 38 | if not os.path.exists('data'): 39 | os.makedirs('data', exist_ok=True) 40 | # 不要在 __init__ 调用任何异步任务! 41 | 42 | async def _file_sha256(self, image_bytes: bytes) -> str: 43 | return hashlib.sha256(image_bytes).hexdigest() 44 | 45 | async def _background_load_cache(self): 46 | # 后台真正懒加载缓存(只在事件循环内调用,不会在__init__强制调动) 47 | if self.cache_loaded or self._load_cache_launched: 48 | return 49 | self._load_cache_launched = True 50 | try: 51 | async with self.cache_lock: 52 | if os.path.exists(UPLOAD_CACHE_FILE): 53 | async with aiofiles.open(UPLOAD_CACHE_FILE, 'r', encoding='utf-8') as f: 54 | content = await f.read() 55 | self.upload_cache = json.loads(content) if content.strip() else {} 56 | else: 57 | self.upload_cache = {} 58 | except Exception as e: 59 | logger.warning(f"后台加载upload_cache失败: {e}") 60 | self.upload_cache = {} 61 | self.cache_loaded = True 62 | 63 | def _save_cache_background(self): 64 | async def _do_save(): 65 | try: 66 | async with self.cache_lock: 67 | async with aiofiles.open(UPLOAD_CACHE_FILE, 'w', encoding='utf-8') as f: 68 | await f.write(json.dumps(self.upload_cache, ensure_ascii=False, indent=2)) 69 | except Exception as e: 70 | logger.warning(f"异步写upload_cache失败: {e}") 71 | try: 72 | loop = asyncio.get_running_loop() 73 | loop.create_task(_do_save()) 74 | except RuntimeError: 75 | # 事件循环未开启,直接跳过 76 | pass 77 | 78 | async def _check_or_set_upload_cache(self, image_bytes: bytes, url: str = None) -> Optional[str]: 79 | # 永远不等待缓存加载,没加载就fire一次后台(只fire不等,安全) 80 | if not self.cache_loaded and not self._load_cache_launched: 81 | try: 82 | loop = asyncio.get_running_loop() 83 | loop.create_task(self._background_load_cache()) 84 | except RuntimeError: 85 | pass # 没有事件循环,服务启动期不用缓存 86 | return None 87 | if not self.cache_loaded: 88 | return None 89 | try: 90 | sha256_digest = await self._file_sha256(image_bytes) 91 | if sha256_digest in self.upload_cache: 92 | return self.upload_cache[sha256_digest] 93 | if url: 94 | self.upload_cache[sha256_digest] = url 95 | self._save_cache_background() 96 | except Exception as e: 97 | logger.warning(f'上传缓存操作异常: {e}') 98 | return None 99 | 100 | def _calculate_signature(self, sts_response: dict, date: str) -> str: 101 | date_stamp = date[:8] 102 | region = sts_response['region'].replace('oss-', '') 103 | credential_scope = f"{date_stamp}/{region}/oss/aliyun_v4_request" 104 | canonical_headers = ( 105 | f"content-type:image/jpeg\n" 106 | f"host:{sts_response['bucketname']}.{sts_response['region']}.aliyuncs.com\n" 107 | f"x-oss-content-sha256:UNSIGNED-PAYLOAD\n" 108 | f"x-oss-date:{date}\n" 109 | f"x-oss-security-token:{sts_response['security_token']}" 110 | ) 111 | signed_headers = "content-type;host;x-oss-content-sha256;x-oss-date;x-oss-security-token" 112 | canonical_request = ( 113 | "PUT\n" 114 | f"/{sts_response['file_path']}\n" 115 | "\n" 116 | f"{canonical_headers}\n" 117 | f"{signed_headers}\n" 118 | "UNSIGNED-PAYLOAD" 119 | ) 120 | string_to_sign = ( 121 | "OSS4-HMAC-SHA256\n" 122 | f"{date}\n" 123 | f"{credential_scope}\n" 124 | f"{sha256(canonical_request.encode('utf-8')).hexdigest()}" 125 | ) 126 | k_date = HMAC(("aliyun_v4" + sts_response['access_key_secret']).encode('utf-8'), 127 | date_stamp.encode('utf-8'), sha256).digest() 128 | k_region = HMAC(k_date, region.encode('utf-8'), sha256).digest() 129 | k_service = HMAC(k_region, b'oss', sha256).digest() 130 | k_signing = HMAC(k_service, b'aliyun_v4_request', sha256).digest() 131 | signature = HMAC(k_signing, string_to_sign.encode('utf-8'), sha256).hexdigest() 132 | return signature 133 | 134 | async def _post_with_retry( 135 | self, 136 | url: str, 137 | headers: dict, 138 | json_data: dict, 139 | timeout: float = 15.0, 140 | *, 141 | max_token_refresh: int = 1, 142 | max_429_retry: int = 5 143 | ) -> Optional[httpx.Response]: 144 | """ 145 | 支持401自动刷新token、429指数退避,返回最终响应 146 | """ 147 | attempt = 0 148 | token_refresh_count = 0 149 | current_headers = dict(headers) 150 | while attempt < max_429_retry: 151 | try: 152 | async with httpx.AsyncClient(timeout=timeout) as client: 153 | resp = await client.post(url, headers=current_headers, json=json_data) 154 | # 401 token失效 155 | if resp.status_code == 401 and token_refresh_count < max_token_refresh: 156 | logger.warning("UploadService检测到401,刷新token后重试...") 157 | account = account_manager.get_account_by_token(headers['authorization'].split(' ')[1]) 158 | new_token_dict = await account_service.login(account['username'], account['password']) 159 | if not new_token_dict: 160 | logger.error("UploadService刷新token失败") 161 | return None 162 | # 更新header 163 | logger.info(f"UploadService刷新token成功: {new_token_dict['token']}") 164 | current_headers = cookie_service.get_headers(new_token_dict['token']) 165 | token_refresh_count += 1 166 | continue 167 | # 429 指数退避 168 | if resp.status_code == 429 and attempt < max_429_retry - 1: 169 | wait_time = 2 ** attempt 170 | logger.warning(f"OSS getstsToken 429, {wait_time}s后重试") 171 | await asyncio.sleep(wait_time) 172 | attempt += 1 173 | continue 174 | if resp.status_code >= 400: 175 | resp.raise_for_status() 176 | return resp 177 | except Exception as e: 178 | logger.error(f"UploadService请求出错: {e}") 179 | return None 180 | return None 181 | 182 | async def _upload_to_oss(self, image_bytes: bytes, auth_token: str) -> Optional[str]: 183 | try: 184 | logger.info("正在获取STS Token...") 185 | url = f"{config_manager.get('api.url', 'https://chat.qwen.ai/api')}/v1/files/getstsToken" 186 | get_headers = cookie_service.get_headers # 保证最新token 187 | token_headers = get_headers(auth_token) 188 | 189 | # 调用带401/429重试的post 190 | resp = await self._post_with_retry( 191 | url, 192 | token_headers, 193 | { 194 | "filename": f"{uuid.uuid4()}.jpg", 195 | "filesize": len(image_bytes), 196 | "filetype": "image" 197 | }, 198 | timeout=15.0 199 | ) 200 | if not resp or resp.status_code != 200: 201 | emsg = f"获取STS Token失败: 状态码={getattr(resp, 'status_code', '无响应')}, 内容={getattr(resp, 'text', '')}" 202 | logger.error(emsg) 203 | return None 204 | sts_data = resp.json() 205 | 206 | credentials_provider = oss.credentials.StaticCredentialsProvider( 207 | access_key_id=sts_data['access_key_id'], 208 | access_key_secret=sts_data['access_key_secret'], 209 | security_token=sts_data['security_token'] 210 | ) 211 | cfg = oss.config.load_default() 212 | cfg.credentials_provider = credentials_provider 213 | region = sts_data['region'].replace('oss-', '') 214 | cfg.region = region 215 | client = oss.Client(cfg) 216 | put_object_request = oss.models.PutObjectRequest( 217 | bucket=sts_data['bucketname'], 218 | key=sts_data['file_path'], 219 | body=image_bytes, 220 | content_type='image/jpeg' 221 | ) 222 | response = client.put_object(put_object_request) 223 | if response.status_code == 200: 224 | logger.info(f"图片上传成功,URL: {sts_data['file_url']}") 225 | return sts_data['file_url'] 226 | else: 227 | error_msg = f"上传图片失败: 状态码={response.status_code}" 228 | logger.error(error_msg) 229 | return None 230 | 231 | except Exception as e: 232 | error_stack = traceback.format_exc() 233 | logger.error(f"上传图片到OSS时出错: {str(e)}\n堆栈跟踪:\n{error_stack}") 234 | return None 235 | 236 | async def save_url(self, url: str, auth_token: Optional[str] = None) -> Optional[str]: 237 | try: 238 | if not auth_token or not url: 239 | return None 240 | 241 | if 'cdn.qwen.ai' in url: 242 | logger.info("检测到OSS URL,直接返回") 243 | return url 244 | 245 | if url.startswith('data:'): 246 | logger.info("处理base64格式的图像数据") 247 | matches = url.split(';base64,') 248 | if len(matches) == 2: 249 | base64_data = matches[1] 250 | else: 251 | base64_data = url.split(',')[1] 252 | image_bytes = base64.b64decode(base64_data) 253 | else: 254 | logger.info(f"从URL下载图像: {url}") 255 | async with httpx.AsyncClient(timeout=15) as client: 256 | response = await client.get(url) 257 | if response.status_code != 200: 258 | logger.error(f"下载图像失败: 状态码={response.status_code}, 响应内容={response.text}") 259 | return None 260 | image_bytes = response.content 261 | 262 | # 判重(缓存未加载/失败不影响业务) 263 | cached_url = await self._check_or_set_upload_cache(image_bytes) 264 | if cached_url: 265 | logger.info(f"缓存命中:SHA256={await self._file_sha256(image_bytes)} / URL={cached_url}") 266 | return cached_url 267 | 268 | uploaded_url = await asyncio.wait_for(self._upload_to_oss(image_bytes, auth_token), timeout=30) 269 | if uploaded_url: 270 | try: 271 | await self._check_or_set_upload_cache(image_bytes, url=uploaded_url) 272 | except Exception as e: 273 | logger.warning(f"上传后写缓存失败: {e}") 274 | return uploaded_url 275 | 276 | return None 277 | except Exception as e: 278 | error_stack = traceback.format_exc() 279 | logger.error(f"处理图像URL失败: {str(e)}\n堆栈跟踪:\n{error_stack}") 280 | return None -------------------------------------------------------------------------------- /config.yaml.example: -------------------------------------------------------------------------------- 1 | api: 2 | api_keys: 3 | - C00NDlkLTljMGEtN2NhMzI5MGUxY2VlIiwicmVzb3VyY2VfaWQiOiJlMGZiN2YzMS00Zjc1LTQyM 4 | debug: false 5 | enable_api_key: false 6 | host: 0.0.0.0 7 | port: 2778 8 | reload: true 9 | url: https://chat.qwen.ai/api 10 | workers: 1 11 | chat: 12 | model: qwen-max-latest 13 | search_info_mode: table 14 | image: 15 | model: qwen-max-latest-draw 16 | size: '1:1' 17 | # 图片尺寸,可选值:1:1, 16:9, 4:3, 3:4, 9:16 18 | log: 19 | file_path: logs/app.log 20 | level: INFO 21 | upload: 22 | enable: true 23 | max_size: 10 24 | save_path: uploads 25 | video: 26 | model: qwen-max-latest-video 27 | size: 1280x720 28 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "qwen2api" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.13" 7 | dependencies = [ 8 | "aiofiles==24.1.0", 9 | "alibabacloud-oss-v2==1.1.0", 10 | "email-validator==2.2.0", 11 | "fastapi==0.110.0", 12 | "httpx>=0.27.0", 13 | "loguru>=0.7.2", 14 | "markupsafe==3.0.2", 15 | "pipdeptree==2.26.0", 16 | "python-dotenv==1.0.1", 17 | "python-multipart==0.0.7", 18 | "pyyaml==6.0.1", 19 | "setuptools==65.5.0", 20 | "uuid==1.30", 21 | "uvicorn==0.28.0", 22 | ] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengwind2006/Qwen2API/f48d2b4241bd1cbfb91268d3b9f6dba0902576cc/requirements.txt -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import os 2 | import uvicorn 3 | from app.main import app, get_start_info 4 | from app.core.logger.logger import get_logger 5 | from app.core.config_manager import ConfigManager 6 | from app.core.logger.logger_config import configure_logging 7 | 8 | configure_logging() 9 | logger = get_logger("run") 10 | config_manager = ConfigManager() 11 | 12 | if __name__ == "__main__": 13 | # 从配置管理器获取配置 14 | listen_address = config_manager.get('api.host') 15 | service_port = config_manager.get('api.port') 16 | reload_enabled = config_manager.get('api.reload', False) 17 | 18 | # 打印启动信息 19 | logger.info(get_start_info()) 20 | 21 | # 启动服务器 22 | uvicorn.run( 23 | "app.main:app", 24 | host=listen_address, 25 | port=service_port, 26 | reload=reload_enabled, 27 | log_config=None, 28 | ) -------------------------------------------------------------------------------- /static/404.html: -------------------------------------------------------------------------------- 1 | Qwen2API Frontend -------------------------------------------------------------------------------- /static/404/index.html: -------------------------------------------------------------------------------- 1 | Qwen2API Frontend -------------------------------------------------------------------------------- /static/_next/static/BXR8W7u2Bte2AoAKInO7B/_buildManifest.js: -------------------------------------------------------------------------------- 1 | self.__BUILD_MANIFEST={__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/_error":["static/chunks/pages/_error-9a890acb1e81c3fc.js"],sortedPages:["/_app","/_error"]},self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB(); -------------------------------------------------------------------------------- /static/_next/static/BXR8W7u2Bte2AoAKInO7B/_ssgManifest.js: -------------------------------------------------------------------------------- 1 | self.__SSG_MANIFEST=new Set([]);self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB() -------------------------------------------------------------------------------- /static/_next/static/chunks/app/_not-found-cf5c4a63df6f8e87.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[165],{3155:function(e,t,n){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_not-found",function(){return n(4032)}])},4032:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"default",{enumerable:!0,get:function(){return i}}),n(6921);let o=n(3827);n(4090);let r={error:{fontFamily:'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',height:"100vh",textAlign:"center",display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center"},desc:{display:"inline-block"},h1:{display:"inline-block",margin:"0 20px 0 0",padding:"0 23px 0 0",fontSize:24,fontWeight:500,verticalAlign:"top",lineHeight:"49px"},h2:{fontSize:14,fontWeight:400,lineHeight:"49px",margin:0}};function i(){return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)("title",{children:"404: This page could not be found."}),(0,o.jsx)("div",{style:r.error,children:(0,o.jsxs)("div",{children:[(0,o.jsx)("style",{dangerouslySetInnerHTML:{__html:"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}),(0,o.jsx)("h1",{className:"next-error-h1",style:r.h1,children:"404"}),(0,o.jsx)("div",{style:r.desc,children:(0,o.jsx)("h2",{style:r.h2,children:"This page could not be found."})})]})})]})}("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),e.exports=t.default)}},function(e){e.O(0,[971,69,744],function(){return e(e.s=3155)}),_N_E=e.O()}]); -------------------------------------------------------------------------------- /static/_next/static/chunks/app/account/login/page-011af162c7b9cfe7.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[326],{5809:function(e,r,t){Promise.resolve().then(t.bind(t,4213))},2372:function(e,r,t){"use strict";t.d(r,{Z:function(){return i}});var s=t(2110),n=t(4090),a={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M858.5 763.6a374 374 0 00-80.6-119.5 375.63 375.63 0 00-119.5-80.6c-.4-.2-.8-.3-1.2-.5C719.5 518 760 444.7 760 362c0-137-111-248-248-248S264 225 264 362c0 82.7 40.5 156 102.8 201.1-.4.2-.8.3-1.2.5-44.8 18.9-85 46-119.5 80.6a375.63 375.63 0 00-80.6 119.5A371.7 371.7 0 00136 901.8a8 8 0 008 8.2h60c4.4 0 7.9-3.5 8-7.8 2-77.2 33-149.5 87.8-204.3 56.7-56.7 132-87.9 212.2-87.9s155.5 31.2 212.2 87.9C779 752.7 810 825 812 902.2c.1 4.4 3.6 7.8 8 7.8h60a8 8 0 008-8.2c-1-47.8-10.9-94.3-29.5-138.2zM512 534c-45.9 0-89.1-17.9-121.6-50.4S340 407.9 340 362c0-45.9 17.9-89.1 50.4-121.6S466.1 190 512 190s89.1 17.9 121.6 50.4S684 316.1 684 362c0 45.9-17.9 89.1-50.4 121.6S557.9 534 512 534z"}}]},name:"user",theme:"outlined"},o=t(688),i=n.forwardRef(function(e,r){return n.createElement(o.Z,(0,s.Z)({},e,{ref:r,icon:a}))})},7907:function(e,r,t){"use strict";var s=t(5313);t.o(s,"usePathname")&&t.d(r,{usePathname:function(){return s.usePathname}}),t.o(s,"useRouter")&&t.d(r,{useRouter:function(){return s.useRouter}}),t.o(s,"useSearchParams")&&t.d(r,{useSearchParams:function(){return s.useSearchParams}}),t.o(s,"useServerInsertedHTML")&&t.d(r,{useServerInsertedHTML:function(){return s.useServerInsertedHTML}})},4213:function(e,r,t){"use strict";t.r(r),t.d(r,{default:function(){return y}});var s=t(3827),n=t(4090),a=t(1945),o=t(8567),i=t(1587),c=t(2110),u={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M699 353h-46.9c-10.2 0-19.9 4.9-25.9 13.3L469 584.3l-71.2-98.8c-6-8.3-15.6-13.3-25.9-13.3H325c-6.5 0-10.3 7.4-6.5 12.7l124.6 172.8a31.8 31.8 0 0051.7 0l210.6-292c3.9-5.3.1-12.7-6.4-12.7z"}},{tag:"path",attrs:{d:"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"}}]},name:"check-circle",theme:"outlined"},l=t(688),d=n.forwardRef(function(e,r){return n.createElement(l.Z,(0,c.Z)({},e,{ref:r,icon:u}))}),m=t(7907),f=t(5175),p=t(9732),h=t(2372),g=t(6541);function P(e){let[r,t]=(0,n.useState)(!1),o=(0,m.useRouter)(),c=(0,m.useSearchParams)().get("redirect"),u=async e=>{t(!0);try{let r=await g.O.validateApiKey(e.apiKey);r.success?(f.ZP.success("登录成功"),o.push(c||"/admin/list")):f.ZP.error(r.error||"登录失败")}catch(e){f.ZP.error((null==e?void 0:e.message)||"登录过程中发生错误")}finally{t(!1)}};return(0,s.jsxs)(a.Z,{name:"login",onFinish:u,layout:"vertical",requiredMark:!1,children:[(0,s.jsx)(a.Z.Item,{name:"apiKey",rules:[{required:!0,message:"请输入API密钥"},{max:50,message:"API密钥不能超过50个字符"}],children:(0,s.jsx)(p.Z,{prefix:(0,s.jsx)(h.Z,{}),placeholder:"API密钥",size:"large",autoComplete:"off"})}),(0,s.jsx)(a.Z.Item,{children:(0,s.jsx)(i.ZP,{type:"primary",htmlType:"submit",size:"large",block:!0,loading:r,children:"登录"})})]})}function y(){let[e,r]=(0,n.useState)(!1),t=(0,m.useRouter)(),[c]=a.Z.useForm(),{isAuthenticated:u}=function(){let[e,r]=(0,n.useState)(!1),[t,s]=(0,n.useState)(!0);return(0,n.useEffect)(()=>{(async()=>{g.O.getApiKey()?r(!0):(s(!1),r(!1))})()},[]),{isAuthenticated:e,isLoading:t}}();return(0,s.jsx)("div",{style:{minHeight:"100vh",background:"linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%)",display:"flex",alignItems:"center",justifyContent:"center"},children:(0,s.jsxs)(o.Z,{style:{minWidth:360,maxWidth:400,width:"100%",borderRadius:8,boxShadow:"0 4px 16px 4px #eaeaec"},bodyStyle:{padding:32},children:[(0,s.jsx)("div",{style:{textAlign:"center",marginBottom:24},children:(0,s.jsxs)("h2",{style:{fontWeight:700,marginBottom:4,color:"#222"},children:[(0,s.jsx)("span",{style:{letterSpacing:3,fontSize:28},children:"Qwen2API"}),(0,s.jsx)("br",{}),u?"欢迎回来":"登录"]})}),u?(0,s.jsxs)("div",{style:{textAlign:"center"},children:[(0,s.jsx)(d,{style:{fontSize:48,color:"#1677ff"}}),(0,s.jsx)("p",{style:{fontSize:16,color:"#333",marginBottom:24},children:"您已成功登录"}),(0,s.jsx)(i.ZP,{type:"primary",size:"large",block:!0,onClick:()=>t.push("/admin/list"),children:"前往管理"})]}):(0,s.jsx)(s.Fragment,{children:(0,s.jsx)(P,{})}),(0,s.jsx)("div",{style:{borderTop:"1px solid #eee",margin:"20px 0 8px 0",paddingTop:14,textAlign:"center",color:"#aaa"},children:(0,s.jsx)("span",{children:"Qwen2API by fengwind"})})]})})}},4865:function(e,r,t){"use strict";t.d(r,{H:function(){return s},P:function(){return n}});let s={baseURL:t(9079).env.NEXT_PUBLIC_API_BASE_URL||"/"},n={accounts:{login:"/accounts/login",logout:e=>"/accounts/logout/".concat(e),list:"/accounts/list",status:e=>"/accounts/".concat(e,"/status"),commonCookies:"/accounts/common-cookies",refresh:e=>"/accounts/".concat(e,"/refresh")},models:{list:"/models",update:e=>"/models/update/".concat(e)}}},8749:function(e,r,t){"use strict";var s=t(3107),n=t(5175),a=t(3742),o=t(4865),i=t(6541),c=t(2825);let u=s.default.create({baseURL:o.H.baseURL,timeout:3e4,withCredentials:!0,headers:{"Content-Type":"application/json"}});u.interceptors.request.use(async e=>{let r=i.O.getApiKey();return r&&(e.headers.Authorization="Bearer ".concat(r)),e},e=>(n.ZP.error("网络错误"),Promise.reject(new c.a(0,"NETWORK_ERROR","网络错误")))),u.interceptors.response.use(e=>e,async e=>{if(e.response){let{status:r,data:t,config:s}=e.response;console.error("API Error:",{status:r,data:t,url:s.url,method:s.method,params:s.params,requestData:s.data});let n=c.a.fromError(e);switch(r){case 400:a.ZP.error({message:"400 请求参数错误",description:n.message||"请求参数错误"});break;case 403:a.ZP.error({message:"403 禁止访问",description:"禁止访问"});break;case 404:a.ZP.error({message:"404 未找到",description:"未找到"});break;case 422:a.ZP.error({message:"422 请求验证错误",description:n.message||"请求验证错误"});break;case 429:a.ZP.warning({message:"429 请求过多",description:n.message||"请求过多"});break;case 500:a.ZP.error({message:"500 服务器错误",description:"服务器错误"});break;default:a.ZP.error({message:"未知错误",description:n.message||"未知错误"})}throw n}if(e.request)throw console.error("No response received:",e.request),a.ZP.error({message:"网络错误",description:"网络错误"}),new c.a(0,"NETWORK_ERROR","网络错误");throw console.error("Request configuration error:",e.message),a.ZP.error({message:"请求错误",description:"请求错误"}),new c.a(0,"REQUEST_FAILED","请求错误")}),r.Z=u},6541:function(e,r,t){"use strict";t.d(r,{O:function(){return c}});var s=t(4865),n=t(8749),a=t(376),o=t(2825);let i={API_KEY:"qwen_api_key"},c={saveApiKey(e){localStorage.setItem(i.API_KEY,e)},getApiKey:()=>localStorage.getItem(i.API_KEY),async validateApiKey(e){try{let r=await n.Z.get("".concat(s.P.models.list),{headers:{Authorization:"Bearer ".concat(e)}});if(200===r.status)return this.saveApiKey(e),{success:!0};return{success:!1,error:"验证失败"}}catch(e){if(e instanceof o.a)return{success:!1,error:e.message||"API 密钥验证失败"};if(e instanceof a.d7){var r,t,i;if((null===(r=e.response)||void 0===r?void 0:r.status)===403)return{success:!1,error:"API 密钥无效或未授权访问"};return{success:!1,error:(null===(i=e.response)||void 0===i?void 0:null===(t=i.data)||void 0===t?void 0:t.message)||"API 密钥验证失败"}}return{success:!1,error:"验证过程中发生未知错误"}}},async logout(){localStorage.removeItem(i.API_KEY)}}},2825:function(e,r,t){"use strict";t.d(r,{a:function(){return s}});class s extends Error{static fromError(e){if(e.response){var r,t;return new s(e.response.status,(null===(r=e.response.data)||void 0===r?void 0:r.code)||"UNKNOWN_ERROR",(null===(t=e.response.data)||void 0===t?void 0:t.detail)||"未知错误")}return new s(0,"UNKNOWN_ERROR",e.message||"未知错误")}constructor(e,r,t){super(t),this.statusCode=e,this.code=r,this.name="ApiException"}}}},function(e){e.O(0,[587,385,205,518,971,69,744],function(){return e(e.s=5809)}),_N_E=e.O()}]); -------------------------------------------------------------------------------- /static/_next/static/chunks/app/admin/cookies/page-2a3752622a163bc5.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[139],{8907:function(e,s,r){Promise.resolve().then(r.bind(r,7286))},7286:function(e,s,r){"use strict";r.r(s),r.d(s,{default:function(){return k}});var t=r(3827),o=r(4090),a=r(5680),n=r(1528),i=r(1945),c=r(5175),l=r(5270),u=r(8567),d=r(1587),m=r(2212),p=r(9732),g=r(8543),h=r(4174),v=r(3172);let{Text:f}=n.default,y=()=>{let[e]=i.Z.useForm(),[s,r]=(0,o.useState)(!1),[n,f]=(0,o.useState)({}),y=(0,o.useCallback)(async()=>{r(!0);try{let s=await g.Q.getCommonCookies();s.cookies&&(f(s.cookies),e.setFieldsValue({cookies:JSON.stringify(s.cookies,null,2)}))}catch(e){c.ZP.error("获取公共 Cookie 失败")}finally{r(!1)}},[e]);(0,o.useEffect)(()=>{y()},[y]);let k=async e=>{try{r(!0),await g.Q.updateCommonCookies(e.cookies),c.ZP.success("更新成功"),y()}catch(e){console.error("Failed to update common cookies:",e),c.ZP.error("更新失败")}finally{r(!1)}},Z=async()=>{try{await navigator.clipboard.writeText(JSON.stringify(n,null,2)),c.ZP.success("已复制到剪贴板")}catch(e){c.ZP.error("复制失败")}},x=Object.entries(n).map(e=>{let[s,r]=e;return{key:s,name:s,value:r}});return(0,t.jsx)(a.Z,{title:"公共 Cookie 管理",children:(0,t.jsxs)(l.Z,{direction:"vertical",style:{width:"100%"},size:"large",children:[(0,t.jsx)(u.Z,{title:"当前 Cookie",extra:(0,t.jsxs)(l.Z,{children:[(0,t.jsx)(d.ZP,{icon:(0,t.jsx)(h.Z,{}),onClick:Z,disabled:0===Object.keys(n).length,children:"复制"}),(0,t.jsx)(d.ZP,{icon:(0,t.jsx)(v.Z,{}),onClick:y,loading:s,children:"刷新"})]}),children:(0,t.jsx)(m.Z,{columns:[{title:"Cookie 名称",dataIndex:"name",key:"name",width:"30%"},{title:"Cookie 值",dataIndex:"value",key:"value",width:"70%",ellipsis:!1,render:e=>(0,t.jsx)("div",{style:{whiteSpace:"pre-wrap",wordBreak:"break-all",padding:"8px 0"},children:e})}],dataSource:x,pagination:!1,size:"small",scroll:{x:!0}})}),(0,t.jsx)(u.Z,{title:"更新 Cookie",children:(0,t.jsxs)(i.Z,{form:e,onFinish:k,layout:"vertical",children:[(0,t.jsx)(i.Z.Item,{label:"Cookie JSON",name:"cookies",rules:[{required:!0,message:"请输入 Cookie"},{validator:async(e,s)=>{if(s)try{JSON.parse(s)}catch(e){throw Error("请输入有效的 JSON 格式")}}}],help:"请输入有效的 JSON 格式的 Cookie 数据",children:(0,t.jsx)(p.Z.TextArea,{rows:10,placeholder:"请输入 JSON 格式的 Cookie 数据"})}),(0,t.jsx)(i.Z.Item,{children:(0,t.jsx)(d.ZP,{type:"primary",htmlType:"submit",loading:s,children:"保存"})})]})})]})})};function k(){return(0,t.jsx)(y,{})}},5680:function(e,s,r){"use strict";var t=r(3827),o=r(4090),a=r(6169),n=r(8188),i=r(8567),c=r(9519),l=r.n(c);s.Z=e=>{let{title:s,subTitle:r,extra:c,children:u,loading:d,footer:m,style:p,className:g="",showHeader:h=!0,contentCard:v=!0}=e,{token:f}=a.Z.useToken(),y=(0,o.useMemo)(()=>d?(0,t.jsx)(n.Z,{active:!0,title:!0,paragraph:!1,style:{width:180,height:38,margin:"8px 0"}}):s&&(0,t.jsx)("h1",{style:{fontSize:f.fontSizeHeading3,fontWeight:600,margin:0},children:s}),[d,s,f.fontSizeHeading3]),k=(0,o.useMemo)(()=>d&&r?(0,t.jsx)(n.Z,{active:!0,title:!1,paragraph:{rows:1,width:"60%"},style:{marginTop:f.marginXS}}):r&&(0,t.jsx)("div",{style:{color:f.colorTextSecondary,marginTop:f.marginXS},children:r}),[d,r,f.colorTextSecondary,f.marginXS]);return(0,t.jsxs)("div",{style:{backgroundColor:"transparent",...p},className:g,children:[h&&(s||r||c)&&(0,t.jsxs)("div",{style:{marginBottom:f.marginLG,display:"flex",justifyContent:"space-between",alignItems:"center"},children:[(0,t.jsxs)("div",{children:[y,k]}),c&&(0,t.jsx)("div",{children:d?null:c})]}),(0,t.jsx)("div",{className:l().pageContainerContent,children:v?(0,t.jsx)(i.Z,{bodyStyle:{padding:0},loading:d,children:u}):d?(0,t.jsx)(n.Z,{active:!0,paragraph:{rows:8}}):u}),m&&(0,t.jsx)("div",{style:{marginTop:f.marginLG},children:d?(0,t.jsx)(n.Z,{active:!0,paragraph:{rows:1,width:["40%"]}}):m})]})}},4865:function(e,s,r){"use strict";r.d(s,{H:function(){return t},P:function(){return o}});let t={baseURL:r(9079).env.NEXT_PUBLIC_API_BASE_URL||"/"},o={accounts:{login:"/accounts/login",logout:e=>"/accounts/logout/".concat(e),list:"/accounts/list",status:e=>"/accounts/".concat(e,"/status"),commonCookies:"/accounts/common-cookies",refresh:e=>"/accounts/".concat(e,"/refresh")},models:{list:"/models",update:e=>"/models/update/".concat(e)}}},8749:function(e,s,r){"use strict";var t=r(3107),o=r(5175),a=r(3742),n=r(4865),i=r(6541),c=r(2825);let l=t.default.create({baseURL:n.H.baseURL,timeout:3e4,withCredentials:!0,headers:{"Content-Type":"application/json"}});l.interceptors.request.use(async e=>{let s=i.O.getApiKey();return s&&(e.headers.Authorization="Bearer ".concat(s)),e},e=>(o.ZP.error("网络错误"),Promise.reject(new c.a(0,"NETWORK_ERROR","网络错误")))),l.interceptors.response.use(e=>e,async e=>{if(e.response){let{status:s,data:r,config:t}=e.response;console.error("API Error:",{status:s,data:r,url:t.url,method:t.method,params:t.params,requestData:t.data});let o=c.a.fromError(e);switch(s){case 400:a.ZP.error({message:"400 请求参数错误",description:o.message||"请求参数错误"});break;case 403:a.ZP.error({message:"403 禁止访问",description:"禁止访问"});break;case 404:a.ZP.error({message:"404 未找到",description:"未找到"});break;case 422:a.ZP.error({message:"422 请求验证错误",description:o.message||"请求验证错误"});break;case 429:a.ZP.warning({message:"429 请求过多",description:o.message||"请求过多"});break;case 500:a.ZP.error({message:"500 服务器错误",description:"服务器错误"});break;default:a.ZP.error({message:"未知错误",description:o.message||"未知错误"})}throw o}if(e.request)throw console.error("No response received:",e.request),a.ZP.error({message:"网络错误",description:"网络错误"}),new c.a(0,"NETWORK_ERROR","网络错误");throw console.error("Request configuration error:",e.message),a.ZP.error({message:"请求错误",description:"请求错误"}),new c.a(0,"REQUEST_FAILED","请求错误")}),s.Z=l},8543:function(e,s,r){"use strict";r.d(s,{Q:function(){return n}});var t=r(4865),o=r(8749),a=r(376);let n={async login(e){try{return await o.Z.post(t.P.accounts.login,e),{success:!0}}catch(e){if(e instanceof a.d7){var s,r;return{success:!1,error:(null===(r=e.response)||void 0===r?void 0:null===(s=r.data)||void 0===s?void 0:s.message)||"登录失败"}}return{success:!1,error:"登录失败"}}},async logout(e){try{return await o.Z.post(t.P.accounts.logout(e)),{success:!0}}catch(e){if(e instanceof a.d7){var s,r;return{success:!1,error:(null===(r=e.response)||void 0===r?void 0:null===(s=r.data)||void 0===s?void 0:s.message)||"登出失败"}}return{success:!1,error:"登出失败"}}},getAccounts:async()=>(await o.Z.get(t.P.accounts.list)).data,async updateAccountStatus(e,s){try{return await o.Z.post(t.P.accounts.status(e),s),{success:!0}}catch(e){if(e instanceof a.d7){var r,n;return{success:!1,error:(null===(n=e.response)||void 0===n?void 0:null===(r=n.data)||void 0===r?void 0:r.message)||"更新账号状态失败"}}return{success:!1,error:"更新账号状态失败"}}},getCommonCookies:async()=>(await o.Z.get(t.P.accounts.commonCookies)).data.data,async updateCommonCookies(e){try{let s=JSON.parse(e);return await o.Z.post(t.P.accounts.commonCookies,{cookies:s}),{success:!0}}catch(e){if(e instanceof a.d7){var s,r;return{success:!1,error:(null===(r=e.response)||void 0===r?void 0:null===(s=r.data)||void 0===s?void 0:s.message)||"更新公共 Cookie 失败"}}return{success:!1,error:"更新公共 Cookie 失败"}}},async refreshCookie(e){try{return await o.Z.post(t.P.accounts.refresh(e)),{success:!0}}catch(e){if(e instanceof a.d7){var s,r;return{success:!1,error:(null===(r=e.response)||void 0===r?void 0:null===(s=r.data)||void 0===s?void 0:s.message)||"刷新 Cookie 失败"}}return{success:!1,error:"刷新 Cookie 失败"}}}}},6541:function(e,s,r){"use strict";r.d(s,{O:function(){return c}});var t=r(4865),o=r(8749),a=r(376),n=r(2825);let i={API_KEY:"qwen_api_key"},c={saveApiKey(e){localStorage.setItem(i.API_KEY,e)},getApiKey:()=>localStorage.getItem(i.API_KEY),async validateApiKey(e){try{let s=await o.Z.get("".concat(t.P.models.list),{headers:{Authorization:"Bearer ".concat(e)}});if(200===s.status)return this.saveApiKey(e),{success:!0};return{success:!1,error:"验证失败"}}catch(e){if(e instanceof n.a)return{success:!1,error:e.message||"API 密钥验证失败"};if(e instanceof a.d7){var s,r,i;if((null===(s=e.response)||void 0===s?void 0:s.status)===403)return{success:!1,error:"API 密钥无效或未授权访问"};return{success:!1,error:(null===(i=e.response)||void 0===i?void 0:null===(r=i.data)||void 0===r?void 0:r.message)||"API 密钥验证失败"}}return{success:!1,error:"验证过程中发生未知错误"}}},async logout(){localStorage.removeItem(i.API_KEY)}}},2825:function(e,s,r){"use strict";r.d(s,{a:function(){return t}});class t extends Error{static fromError(e){if(e.response){var s,r;return new t(e.response.status,(null===(s=e.response.data)||void 0===s?void 0:s.code)||"UNKNOWN_ERROR",(null===(r=e.response.data)||void 0===r?void 0:r.detail)||"未知错误")}return new t(0,"UNKNOWN_ERROR",e.message||"未知错误")}constructor(e,s,r){super(r),this.statusCode=e,this.code=s,this.name="ApiException"}}},9519:function(e){e.exports={"page-container":"PageContainer_page-container__F2_vX",mobile:"PageContainer_mobile__HDFuu","mobile-content":"PageContainer_mobile-content__QrMA_","page-container-content":"PageContainer_page-container-content__jmDFL"}}},function(e){e.O(0,[587,385,205,518,610,212,492,971,69,744],function(){return e(e.s=8907)}),_N_E=e.O()}]); -------------------------------------------------------------------------------- /static/_next/static/chunks/app/admin/layout-4edb65843458ac27.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[91],{2845:function(e,r,t){Promise.resolve().then(t.bind(t,4158))},4158:function(e,r,t){"use strict";t.r(r),t.d(r,{default:function(){return L}});var n=t(3827),o=t(4090),i=t(4440),s=t(6169),l=t(9974),a=t(8188),c=t(5334),d=t(3582),u=t(7297),p=t(8807),h=t(311),g=t(7907),f=t(2372),y=t(4523);let m=function e(r,t){return r.map(r=>{var n;let o=null!==(n=r.auth)&&void 0!==n?n:t,i={...r,effectiveAuth:o};return r.children&&(i.children=e(r.children,o)),i})}(function e(r){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";return r.map(r=>{let n=r.path||r.key,o=["link","divider","slot"].includes(r.type);"group"===r.type&&(o=!r.routeableForGroup);let i=o?void 0:"".concat(t?t+"/":"/").concat(n).replace(/\/+/g,"/"),s={...r,path:o?void 0:i,key:o?r.key:i};return r.children&&(s.children=e(r.children,null!=i?i:"")),s})}([{key:"admin",label:"管理",icon:(0,n.jsx)(f.Z,{}),order:1,auth:{actions:["accounts:list"]},type:"group",routeableForGroup:!0,noPage:!0,children:[{key:"cookies",label:"公共 Cookie",icon:(0,n.jsx)(y.Z,{}),order:1,type:"item"},{key:"list",label:"账号管理",icon:(0,n.jsx)(f.Z,{}),type:"item",order:2}]}]));function x(e){let r={};return!function e(t,n){for(let o of t)r[o.key]={...o,parent:n},o.children&&e(o.children,o.key)}(e),r}var b=t(1445);let{Sider:v}=i.default;function k(e){let{collapsed:r,isMobile:t=!1,onMenuClick:i,slots:s={}}=e,l=(0,g.useRouter)(),a=(0,g.usePathname)(),c=(0,o.useMemo)(()=>(function e(r,t){return r.filter(e=>!e.hideInMenu).map(r=>r.children?{...r,children:e(r.children,t)}:r).filter(e=>!e.children||e.children.length>0).sort((e,r)=>{var t,n;return(null!==(t=e.order)&&void 0!==t?t:0)-(null!==(n=r.order)&&void 0!==n?n:0)})})(m,{}),[]),d=(0,o.useMemo)(()=>(function e(r,t){return r.map(r=>"group"===r.type?{type:"group",key:r.key,label:r.label,children:r.children?e(r.children,t):[]}:"divider"===r.type?{type:"divider"}:"link"===r.type&&r.url?{key:r.key,icon:r.icon,label:r.label}:"slot"===r.type&&r.slotKey&&t&&t[r.slotKey]?{key:r.key,label:t[r.slotKey],disabled:!0,style:{cursor:"auto",background:"transparent",padding:0}}:{key:r.key,icon:r.icon,label:(0,n.jsx)("a",{href:r.key,style:{color:"inherit",textDecoration:"none"},onClick:e=>{e.metaKey||e.ctrlKey||1===e.button||e.preventDefault()},children:r.label}),children:r.children?e(r.children,t):void 0})})(c,s),[c,s]),u=(0,o.useMemo)(()=>x(m),[]),f=(0,o.useMemo)(()=>(function(e,r){let t=r;for(;t;){let r=e[t];if(r&&"item"===r.type&&!r.noPage)return r.key;if(t.lastIndexOf("/")>0)t=t.substring(0,t.lastIndexOf("/"));else break}return""})(u,a),[a,u]),y=(0,o.useMemo)(()=>f?function(e,r){var t,n;let o=[],i=null===(t=e[r])||void 0===t?void 0:t.parent;for(;i;)o.unshift(i),i=null===(n=e[i])||void 0===n?void 0:n.parent;return o}(u,f):[],[f,u]);return t?null:(0,n.jsxs)(v,{trigger:null,collapsible:!0,collapsed:r,width:220,collapsedWidth:60,style:{boxShadow:"0 2px 8px rgba(0, 0, 0, 0.15)",zIndex:10,height:"100vh",position:"fixed",left:0,top:0,overflow:"hidden"},children:[(0,n.jsx)("div",{style:{height:64,display:"flex",alignItems:"center",justifyContent:r?"center":"flex-start",padding:r?"0":"0 16px",borderBottom:"1px solid #f0f0f0"},children:r?(0,n.jsx)(b.Z,{style:{fontSize:20}}):(0,n.jsx)("span",{style:{fontSize:18,fontWeight:600,letterSpacing:3},children:"Qwen2API"})}),0===d.length?(0,n.jsx)("div",{style:{flex:1,display:"flex",justifyContent:"center",alignItems:"center"},children:r?(0,n.jsx)(p.Z,{description:null,image:p.Z.PRESENTED_IMAGE_SIMPLE}):(0,n.jsx)(p.Z,{description:"暂无可用菜单",image:p.Z.PRESENTED_IMAGE_SIMPLE})}):(0,n.jsx)(h.Z,{mode:"inline",selectedKeys:f?[f]:[],defaultOpenKeys:y,items:d,onClick:e=>{let{key:r,domEvent:t}=e,n=u[r];if(n){if("link"===n.type&&n.url){window.open(n.url,"_blank","noopener,noreferrer");return}if(n.noPage||"item"!==n.type){t.preventDefault();return}l.push(r),i&&i()}},style:{borderRight:0,flex:1,background:"transparent",fontWeight:500,fontSize:15,minHeight:"calc(100vh - 65px - 56px)"}})]})}var j=t(9102),w=t.n(j),E=t(6180),P=t(9197),Z=t(7397),I=t(516),R=t(6656),C=t(4848),_=t(6541);let{Header:S}=i.default;function A(e){let{children:r}=e,t=(0,g.useRouter)(),{theme:o,setTheme:i}=(0,C.F)(),{token:l}=s.Z.useToken(),a="dark"===o,c=[{key:"logout",label:"退出登录",icon:(0,n.jsx)(I.Z,{}),onClick:()=>{_.O.logout(),t.push("/account/login"),t.refresh()},danger:!0}];return(0,n.jsxs)(S,{style:{padding:"0 24px",background:l.colorBgContainer,boxShadow:"0 1px 4px rgba(0,0,0,.1)",display:"flex",alignItems:"center",justifyContent:"space-between",zIndex:9},children:[(0,n.jsx)("div",{style:{display:"flex",alignItems:"center"},className:w().dynamic([["ee9b8710879dc233",[l.colorBgTextHover]]]),children:r}),(0,n.jsxs)("div",{style:{display:"flex",alignItems:"center",gap:8},className:w().dynamic([["ee9b8710879dc233",[l.colorBgTextHover]]]),children:[(0,n.jsx)(E.Z,{title:a?"切换到亮色模式":"切换到暗色模式",children:(0,n.jsx)("div",{onClick:()=>i(a?"light":"dark"),style:{height:40,width:40,display:"flex",alignItems:"center",justifyContent:"center",cursor:"pointer",borderRadius:"50%",transition:"background 0.3s",backgroundColor:"transparent"},className:w().dynamic([["ee9b8710879dc233",[l.colorBgTextHover]]])+" header-icon-btn",children:(0,n.jsx)(R.Z,{style:{fontSize:16}})})}),(0,n.jsx)(P.Z,{menu:{items:c},placement:"bottomRight",trigger:["click"],arrow:!0,children:(0,n.jsx)("div",{style:{height:40,width:40,display:"flex",alignItems:"center",justifyContent:"center",cursor:"pointer",borderRadius:"50%",transition:"background 0.3s",backgroundColor:"transparent"},className:w().dynamic([["ee9b8710879dc233",[l.colorBgTextHover]]])+" header-avatar-container",children:(0,n.jsx)(Z.Z,{icon:(0,n.jsx)(f.Z,{}),style:{backgroundColor:l.colorPrimary},size:"small"})})})]}),(0,n.jsx)(w(),{id:"ee9b8710879dc233",dynamic:[l.colorBgTextHover],children:".header-icon-btn:hover,.header-avatar-container:hover{background-color:".concat(l.colorBgTextHover,"}")})]})}var N=t(4347),K=t(8792);function B(){let e=(0,g.usePathname)(),r=(0,o.useMemo)(()=>x(m),[]),t=[{key:"/",label:"首页",link:"/"},...(0,o.useMemo)(()=>(function(e,r){let t=r,n=e[t];for(;!n||!("item"===n.type||"group"===n.type&&n.routeableForGroup);)if(t.lastIndexOf("/")>0)n=e[t=t.substring(0,t.lastIndexOf("/"))];else{n=void 0;break}let o=[],i=n;for(;i;)o.unshift(i),i=i.parent&&e[i.parent];return o})(r,e),[r,e]).map(e=>({key:e.key,label:(0,n.jsxs)(n.Fragment,{children:[e.icon?(0,n.jsx)("span",{style:{marginRight:4},children:e.icon}):null,e.label]}),link:"item"!==e.type||e.noPage?void 0:e.key}))];return(0,n.jsx)(N.Z,{items:t.map(e=>{let{key:r,label:t,link:o}=e;return o?{title:(0,n.jsx)(K.default,{href:o,children:t})}:{title:t}})})}let{Content:O}=i.default;function L(e){let{children:r,loading:t=!1}=e,[p,h]=(0,o.useState)(()=>window.innerWidth<=768),[g,f]=(0,o.useState)(()=>window.innerWidth<=768),[y,m]=(0,o.useState)(!1),{token:x}=s.Z.useToken();(0,o.useEffect)(()=>{let e=()=>{let e=window.innerWidth<=768;h(e),f(e)};return window.addEventListener("resize",e),()=>window.removeEventListener("resize",e)},[]);let b=()=>m(!1);return(0,n.jsxs)(i.default,{style:{minHeight:"100vh",position:"relative",flexDirection:"row",background:x.colorBgLayout},children:[(0,n.jsx)(k,{collapsed:g,isMobile:p}),p&&(0,n.jsx)(l.Z,{title:"菜单",placement:"left",onClose:b,open:y,width:220,bodyStyle:{padding:0},children:(0,n.jsx)(k,{collapsed:!1,onMenuClick:b})},"mobile-sidebar"),(0,n.jsxs)(i.default,{style:{marginLeft:p?0:g?60:220,transition:"margin-left 0.2s",willChange:"margin-left",display:"flex",flexDirection:"column",height:"100vh",background:x.colorBgLayout},children:[(0,n.jsxs)("div",{style:{flex:"none"},children:[(0,n.jsx)(A,{children:p?(0,n.jsx)("span",{className:"trigger",onClick:()=>m(!0),style:{cursor:"pointer",fontSize:18},children:(0,n.jsx)(c.Z,{})}):(0,n.jsx)("span",{className:"trigger",onClick:()=>f(!g),style:{cursor:"pointer",fontSize:18},children:g?(0,n.jsx)(d.Z,{}):(0,n.jsx)(u.Z,{})})}),(0,n.jsx)("div",{style:{padding:"12px 24px",background:x.colorBgLayout,borderBottom:"1px solid ".concat(x.colorBorderSecondary)},children:(0,n.jsx)(B,{})})]}),(0,n.jsx)(O,{style:{flex:1,minHeight:0,overflow:"auto",background:x.colorBgLayout},children:(0,n.jsx)("div",{style:{padding:24,minHeight:280,margin:"16px",background:x.colorBgContainer,borderRadius:x.borderRadiusLG,transition:"background 0.2s"},children:t?(0,n.jsx)(a.Z,{active:!0,paragraph:{rows:8}}):r})})]})]})}},4865:function(e,r,t){"use strict";t.d(r,{H:function(){return n},P:function(){return o}});let n={baseURL:t(9079).env.NEXT_PUBLIC_API_BASE_URL||"/"},o={accounts:{login:"/accounts/login",logout:e=>"/accounts/logout/".concat(e),list:"/accounts/list",status:e=>"/accounts/".concat(e,"/status"),commonCookies:"/accounts/common-cookies",refresh:e=>"/accounts/".concat(e,"/refresh")},models:{list:"/models",update:e=>"/models/update/".concat(e)}}},8749:function(e,r,t){"use strict";var n=t(3107),o=t(5175),i=t(3742),s=t(4865),l=t(6541),a=t(2825);let c=n.default.create({baseURL:s.H.baseURL,timeout:3e4,withCredentials:!0,headers:{"Content-Type":"application/json"}});c.interceptors.request.use(async e=>{let r=l.O.getApiKey();return r&&(e.headers.Authorization="Bearer ".concat(r)),e},e=>(o.ZP.error("网络错误"),Promise.reject(new a.a(0,"NETWORK_ERROR","网络错误")))),c.interceptors.response.use(e=>e,async e=>{if(e.response){let{status:r,data:t,config:n}=e.response;console.error("API Error:",{status:r,data:t,url:n.url,method:n.method,params:n.params,requestData:n.data});let o=a.a.fromError(e);switch(r){case 400:i.ZP.error({message:"400 请求参数错误",description:o.message||"请求参数错误"});break;case 403:i.ZP.error({message:"403 禁止访问",description:"禁止访问"});break;case 404:i.ZP.error({message:"404 未找到",description:"未找到"});break;case 422:i.ZP.error({message:"422 请求验证错误",description:o.message||"请求验证错误"});break;case 429:i.ZP.warning({message:"429 请求过多",description:o.message||"请求过多"});break;case 500:i.ZP.error({message:"500 服务器错误",description:"服务器错误"});break;default:i.ZP.error({message:"未知错误",description:o.message||"未知错误"})}throw o}if(e.request)throw console.error("No response received:",e.request),i.ZP.error({message:"网络错误",description:"网络错误"}),new a.a(0,"NETWORK_ERROR","网络错误");throw console.error("Request configuration error:",e.message),i.ZP.error({message:"请求错误",description:"请求错误"}),new a.a(0,"REQUEST_FAILED","请求错误")}),r.Z=c},6541:function(e,r,t){"use strict";t.d(r,{O:function(){return a}});var n=t(4865),o=t(8749),i=t(376),s=t(2825);let l={API_KEY:"qwen_api_key"},a={saveApiKey(e){localStorage.setItem(l.API_KEY,e)},getApiKey:()=>localStorage.getItem(l.API_KEY),async validateApiKey(e){try{let r=await o.Z.get("".concat(n.P.models.list),{headers:{Authorization:"Bearer ".concat(e)}});if(200===r.status)return this.saveApiKey(e),{success:!0};return{success:!1,error:"验证失败"}}catch(e){if(e instanceof s.a)return{success:!1,error:e.message||"API 密钥验证失败"};if(e instanceof i.d7){var r,t,l;if((null===(r=e.response)||void 0===r?void 0:r.status)===403)return{success:!1,error:"API 密钥无效或未授权访问"};return{success:!1,error:(null===(l=e.response)||void 0===l?void 0:null===(t=l.data)||void 0===t?void 0:t.message)||"API 密钥验证失败"}}return{success:!1,error:"验证过程中发生未知错误"}}},async logout(){localStorage.removeItem(l.API_KEY)}}},2825:function(e,r,t){"use strict";t.d(r,{a:function(){return n}});class n extends Error{static fromError(e){if(e.response){var r,t;return new n(e.response.status,(null===(r=e.response.data)||void 0===r?void 0:r.code)||"UNKNOWN_ERROR",(null===(t=e.response.data)||void 0===t?void 0:t.detail)||"未知错误")}return new n(0,"UNKNOWN_ERROR",e.message||"未知错误")}constructor(e,r,t){super(t),this.statusCode=e,this.code=r,this.name="ApiException"}}}},function(e){e.O(0,[587,385,205,610,250,513,971,69,744],function(){return e(e.s=2845)}),_N_E=e.O()}]); -------------------------------------------------------------------------------- /static/_next/static/chunks/app/admin/list/page-72d2f77d5b74356a.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[845],{4313:function(e,r,n){Promise.resolve().then(n.bind(n,4386))},4386:function(e,r,n){"use strict";n.r(r),n.d(r,{default:function(){return b}});var t=n(3827),s=n(4090),o=n(5680),i=n(1587),a=n(2503),l=n(5270),c=n(9197),d=n(2212),u=n(3618),m=n(1124);function p(e,r){let n=arguments.length>2&&void 0!==arguments[2]&&arguments[2],s=arguments.length>3?arguments[3]:void 0,o="function"==typeof e.disabled?e.disabled(r):e.disabled;if(e.render)return(0,t.jsx)("span",{children:e.render(r)},e.key);let l=(0,t.jsx)(i.ZP,{type:e.type,icon:e.icon,danger:e.danger,size:"small",disabled:o,block:n,onClick:n=>{var t;n.stopPropagation(),null===(t=e.onClick)||void 0===t||t.call(e,r),null==s||s()},children:e.text},e.key);if(e.popConfirm){let{title:i,okText:c,cancelText:d,onConfirm:u,onCancel:m,disabled:p}=e.popConfirm,h="function"==typeof p?p(r):p;return n?(0,t.jsx)("div",{style:{width:"100%"},children:(0,t.jsx)(a.Z,{title:"function"==typeof i?i(r):i,okText:null!=c?c:"确定",cancelText:null!=d?d:"取消",onConfirm:n=>{u&&u(r),e.onClick&&e.onClick(r),null==s||s()},onCancel:()=>{null==m||m(r),null==s||s()},disabled:h||o,children:l},e.key)}):(0,t.jsx)(a.Z,{title:"function"==typeof i?i(r):i,okText:null!=c?c:"确定",cancelText:null!=d?d:"取消",onConfirm:n=>{u&&u(r),e.onClick&&e.onClick(r),null==s||s()},onCancel:()=>{null==m||m(r),null==s||s()},disabled:h||o,children:l},e.key)}return n?(0,t.jsx)("div",{style:{width:"100%"},children:l}):l}function h(e){let{dataSource:r,loading:n,columns:o,rowKey:a,pagination:h,actions:g,mobileFixedActionKeys:f=[],onChange:y}=e,v=(0,m.ac)({maxWidth:768}),x=(0,s.useMemo)(()=>o.filter(e=>!!e.mobileShowInMenu),[o]),k=(0,s.useCallback)(e=>x.map(r=>{let n=r.menuLabelRender?r.menuLabelRender(e):r.render?r.render(r.dataIndex?e[r.dataIndex]:void 0,e,0):r.dataIndex?e[r.dataIndex]:null;return{key:"info-"+String(r.key),label:(0,t.jsxs)("div",{style:{fontSize:13,lineHeight:1.6,display:"flex",alignItems:"center"},children:[r.menuIcon&&(0,t.jsx)("span",{style:{marginRight:6,verticalAlign:"middle",display:"inline-flex",alignItems:"center"},children:r.menuIcon}),(0,t.jsxs)("span",{style:{marginRight:6},children:["function"==typeof r.title?r.title({}):r.title,":"]}),(0,t.jsx)("span",{children:null!=n?n:"-"})]})}}),[x]),Z=(0,s.useCallback)(e=>{var r,n;let{record:o}=e,[a,d]=(0,s.useState)(!1),m=(0,s.useMemo)(()=>g?g(o).filter(e=>!("function"==typeof e.hidden?e.hidden(o):e.hidden)).sort((e,r)=>{var n,t;return(null!==(n=e.order)&&void 0!==n?n:100)-(null!==(t=r.order)&&void 0!==t?t:100)}):[],[g,o]);if(!v){let e=m.filter(e=>!1!==e.showInDesktop);return(0,t.jsx)(l.Z,{children:e.map(e=>p(e,o))})}let h=m.filter(e=>!1!==e.showInMobile),y=[],x=[];f&&f.length>0?(y=h.filter(e=>f.includes(e.key)),x=h.filter(e=>!f.includes(e.key))):x=h;let Z=null!==(r=k(o))&&void 0!==r?r:[],j=x.map(e=>{let r=(0,t.jsx)("div",{style:{margin:"-4px -12px",padding:0},onClick:e=>e.stopPropagation(),children:p(e,o,!0,()=>d(!1))});return[e.dividerBefore?{type:"divider"}:void 0,{key:e.key,icon:void 0,label:r,disabled:"function"==typeof e.disabled?e.disabled(o):e.disabled,danger:e.danger,onClick:()=>{if(!e.popConfirm){var r;null===(r=e.onClick)||void 0===r||r.call(e,o),d(!1)}}}].filter(Boolean)}).flat(),C=[...null!=Z?Z:[],(null!==(n=null==Z?void 0:Z.length)&&void 0!==n?n:0)>0&&j.length?{type:"divider"}:null,...j].filter(Boolean);return(0,t.jsxs)(l.Z,{children:[y.map(e=>p(e,o,!1,()=>d(!1))),(0,t.jsx)(c.Z,{trigger:["click"],placement:"bottomRight",open:a,onOpenChange:e=>d(e),menu:{items:C,onClick:e=>{d(!1);let r=x.find(r=>r.key===e.key);r&&!r.popConfirm&&r.onClick&&r.onClick(o)}},children:(0,t.jsx)(i.ZP,{icon:(0,t.jsx)(u.Z,{}),type:"primary",size:"small",children:"更多"})})]})},[g,k,v,f]),j=(0,s.useMemo)(()=>o.map(e=>("action"===e.key||"actions"===e.key)&&g?{...e,render:(e,r)=>(0,t.jsx)(Z,{record:r})}:e),[o,g,Z]);return(0,t.jsx)(d.Z,{columns:j,dataSource:r,rowKey:a,loading:n,pagination:h,scroll:v?{x:"max-content"}:void 0,size:v?"small":"middle",onChange:y})}var g=n(5175),f=n(7628),y=n(1945),v=n(9732),x=n(6992),k=n(4190),Z=n(8543),j=n(9539),C=n.n(j);let P=()=>{let[e,r]=(0,s.useState)([]),[n,a]=(0,s.useState)(!1),[c,d]=(0,s.useState)(null),[u,m]=(0,s.useState)(!1),[p,j]=(0,s.useState)(!1),P=(0,s.useCallback)(async()=>{a(!0);try{let e=await Z.Q.getAccounts();r(e)}catch(e){g.ZP.error("获取账号列表失败")}finally{a(!1)}},[]);(0,s.useEffect)(()=>{P()},[P]);let b=async e=>{try{let r=await Z.Q.login(e);r.success?(g.ZP.success("登录成功"),j(!1),P()):g.ZP.error(r.error||"登录失败")}catch(e){g.ZP.error("登录失败")}},w=async e=>{try{let r=await Z.Q.logout(e);r.success?(g.ZP.success("登出成功"),P()):g.ZP.error(r.error||"登出失败")}catch(e){g.ZP.error("登出失败")}},_=async e=>{try{let{username:r,...n}=e,t=await Z.Q.updateAccountStatus(r,n);t.success?(g.ZP.success("更新状态成功"),m(!1),P()):g.ZP.error(t.error||"更新状态失败")}catch(e){g.ZP.error("更新状态失败")}},I=async e=>{try{let r=await Z.Q.refreshCookie(e);r.success?(g.ZP.success("刷新 Cookie 成功"),P()):g.ZP.error(r.error||"刷新 Cookie 失败")}catch(e){g.ZP.error("刷新 Cookie 失败")}};return(0,t.jsxs)(o.Z,{title:"账号管理",children:[(0,t.jsx)("div",{style:{marginBottom:16},children:(0,t.jsx)(i.ZP,{type:"primary",onClick:()=>j(!0),children:"账号登录"})}),(0,t.jsx)(h,{columns:[{title:"用户名",dataIndex:"username",key:"username"},{title:"状态",dataIndex:"enabled",key:"enabled",render:e=>(0,t.jsx)("span",{style:{color:e?"#52c41a":"#ff4d4f"},children:e?"启用":"禁用"})},{title:"过期时间",dataIndex:"expires_at",key:"expires_at",render:e=>e?C()(1e3*e).format("YYYY-MM-DD HH:mm:ss"):"-"},{title:"操作",key:"action",render:(e,r)=>(0,t.jsxs)(l.Z,{children:[(0,t.jsx)(i.ZP,{type:"link",onClick:()=>w(r.username),children:"登出"}),(0,t.jsx)(i.ZP,{type:"link",onClick:()=>{d(r),m(!0)},children:"修改状态"}),(0,t.jsx)(i.ZP,{type:"link",onClick:()=>I(r.username),children:"刷新 Cookie"})]})}],dataSource:e,loading:n,rowKey:"username"}),(0,t.jsx)(f.Z,{title:"修改账号状态",open:u,onCancel:()=>m(!1),footer:null,children:(0,t.jsxs)(y.Z,{initialValues:{username:null==c?void 0:c.username,enabled:null==c?void 0:c.enabled,expires_at:(null==c?void 0:c.expires_at)?C()(c.expires_at):void 0},onFinish:_,children:[(0,t.jsx)(y.Z.Item,{name:"username",hidden:!0,children:(0,t.jsx)(v.Z,{})}),(0,t.jsx)(y.Z.Item,{label:"启用状态",name:"enabled",children:(0,t.jsx)(x.Z,{})}),(0,t.jsx)(y.Z.Item,{label:"过期时间",name:"expires_at",children:(0,t.jsx)(k.Z,{showTime:!0})}),(0,t.jsx)(y.Z.Item,{children:(0,t.jsx)(i.ZP,{type:"primary",htmlType:"submit",children:"确认"})})]})}),(0,t.jsx)(f.Z,{title:"账号登录",open:p,onCancel:()=>j(!1),footer:null,children:(0,t.jsxs)(y.Z,{onFinish:b,layout:"vertical",children:[(0,t.jsx)(y.Z.Item,{label:"用户名",name:"username",rules:[{required:!0,message:"请输入用户名"}],children:(0,t.jsx)(v.Z,{placeholder:"请输入用户名"})}),(0,t.jsx)(y.Z.Item,{label:"密码",name:"password",rules:[{required:!0,message:"请输入密码"}],children:(0,t.jsx)(v.Z.Password,{placeholder:"请输入密码"})}),(0,t.jsx)(y.Z.Item,{children:(0,t.jsx)(i.ZP,{type:"primary",htmlType:"submit",block:!0,children:"登录"})})]})})]})};function b(){return(0,t.jsx)(P,{})}},5680:function(e,r,n){"use strict";var t=n(3827),s=n(4090),o=n(6169),i=n(8188),a=n(8567),l=n(9519),c=n.n(l);r.Z=e=>{let{title:r,subTitle:n,extra:l,children:d,loading:u,footer:m,style:p,className:h="",showHeader:g=!0,contentCard:f=!0}=e,{token:y}=o.Z.useToken(),v=(0,s.useMemo)(()=>u?(0,t.jsx)(i.Z,{active:!0,title:!0,paragraph:!1,style:{width:180,height:38,margin:"8px 0"}}):r&&(0,t.jsx)("h1",{style:{fontSize:y.fontSizeHeading3,fontWeight:600,margin:0},children:r}),[u,r,y.fontSizeHeading3]),x=(0,s.useMemo)(()=>u&&n?(0,t.jsx)(i.Z,{active:!0,title:!1,paragraph:{rows:1,width:"60%"},style:{marginTop:y.marginXS}}):n&&(0,t.jsx)("div",{style:{color:y.colorTextSecondary,marginTop:y.marginXS},children:n}),[u,n,y.colorTextSecondary,y.marginXS]);return(0,t.jsxs)("div",{style:{backgroundColor:"transparent",...p},className:h,children:[g&&(r||n||l)&&(0,t.jsxs)("div",{style:{marginBottom:y.marginLG,display:"flex",justifyContent:"space-between",alignItems:"center"},children:[(0,t.jsxs)("div",{children:[v,x]}),l&&(0,t.jsx)("div",{children:u?null:l})]}),(0,t.jsx)("div",{className:c().pageContainerContent,children:f?(0,t.jsx)(a.Z,{bodyStyle:{padding:0},loading:u,children:d}):u?(0,t.jsx)(i.Z,{active:!0,paragraph:{rows:8}}):d}),m&&(0,t.jsx)("div",{style:{marginTop:y.marginLG},children:u?(0,t.jsx)(i.Z,{active:!0,paragraph:{rows:1,width:["40%"]}}):m})]})}},4865:function(e,r,n){"use strict";n.d(r,{H:function(){return t},P:function(){return s}});let t={baseURL:n(9079).env.NEXT_PUBLIC_API_BASE_URL||"/"},s={accounts:{login:"/accounts/login",logout:e=>"/accounts/logout/".concat(e),list:"/accounts/list",status:e=>"/accounts/".concat(e,"/status"),commonCookies:"/accounts/common-cookies",refresh:e=>"/accounts/".concat(e,"/refresh")},models:{list:"/models",update:e=>"/models/update/".concat(e)}}},8749:function(e,r,n){"use strict";var t=n(3107),s=n(5175),o=n(3742),i=n(4865),a=n(6541),l=n(2825);let c=t.default.create({baseURL:i.H.baseURL,timeout:3e4,withCredentials:!0,headers:{"Content-Type":"application/json"}});c.interceptors.request.use(async e=>{let r=a.O.getApiKey();return r&&(e.headers.Authorization="Bearer ".concat(r)),e},e=>(s.ZP.error("网络错误"),Promise.reject(new l.a(0,"NETWORK_ERROR","网络错误")))),c.interceptors.response.use(e=>e,async e=>{if(e.response){let{status:r,data:n,config:t}=e.response;console.error("API Error:",{status:r,data:n,url:t.url,method:t.method,params:t.params,requestData:t.data});let s=l.a.fromError(e);switch(r){case 400:o.ZP.error({message:"400 请求参数错误",description:s.message||"请求参数错误"});break;case 403:o.ZP.error({message:"403 禁止访问",description:"禁止访问"});break;case 404:o.ZP.error({message:"404 未找到",description:"未找到"});break;case 422:o.ZP.error({message:"422 请求验证错误",description:s.message||"请求验证错误"});break;case 429:o.ZP.warning({message:"429 请求过多",description:s.message||"请求过多"});break;case 500:o.ZP.error({message:"500 服务器错误",description:"服务器错误"});break;default:o.ZP.error({message:"未知错误",description:s.message||"未知错误"})}throw s}if(e.request)throw console.error("No response received:",e.request),o.ZP.error({message:"网络错误",description:"网络错误"}),new l.a(0,"NETWORK_ERROR","网络错误");throw console.error("Request configuration error:",e.message),o.ZP.error({message:"请求错误",description:"请求错误"}),new l.a(0,"REQUEST_FAILED","请求错误")}),r.Z=c},8543:function(e,r,n){"use strict";n.d(r,{Q:function(){return i}});var t=n(4865),s=n(8749),o=n(376);let i={async login(e){try{return await s.Z.post(t.P.accounts.login,e),{success:!0}}catch(e){if(e instanceof o.d7){var r,n;return{success:!1,error:(null===(n=e.response)||void 0===n?void 0:null===(r=n.data)||void 0===r?void 0:r.message)||"登录失败"}}return{success:!1,error:"登录失败"}}},async logout(e){try{return await s.Z.post(t.P.accounts.logout(e)),{success:!0}}catch(e){if(e instanceof o.d7){var r,n;return{success:!1,error:(null===(n=e.response)||void 0===n?void 0:null===(r=n.data)||void 0===r?void 0:r.message)||"登出失败"}}return{success:!1,error:"登出失败"}}},getAccounts:async()=>(await s.Z.get(t.P.accounts.list)).data,async updateAccountStatus(e,r){try{return await s.Z.post(t.P.accounts.status(e),r),{success:!0}}catch(e){if(e instanceof o.d7){var n,i;return{success:!1,error:(null===(i=e.response)||void 0===i?void 0:null===(n=i.data)||void 0===n?void 0:n.message)||"更新账号状态失败"}}return{success:!1,error:"更新账号状态失败"}}},getCommonCookies:async()=>(await s.Z.get(t.P.accounts.commonCookies)).data.data,async updateCommonCookies(e){try{let r=JSON.parse(e);return await s.Z.post(t.P.accounts.commonCookies,{cookies:r}),{success:!0}}catch(e){if(e instanceof o.d7){var r,n;return{success:!1,error:(null===(n=e.response)||void 0===n?void 0:null===(r=n.data)||void 0===r?void 0:r.message)||"更新公共 Cookie 失败"}}return{success:!1,error:"更新公共 Cookie 失败"}}},async refreshCookie(e){try{return await s.Z.post(t.P.accounts.refresh(e)),{success:!0}}catch(e){if(e instanceof o.d7){var r,n;return{success:!1,error:(null===(n=e.response)||void 0===n?void 0:null===(r=n.data)||void 0===r?void 0:r.message)||"刷新 Cookie 失败"}}return{success:!1,error:"刷新 Cookie 失败"}}}}},6541:function(e,r,n){"use strict";n.d(r,{O:function(){return l}});var t=n(4865),s=n(8749),o=n(376),i=n(2825);let a={API_KEY:"qwen_api_key"},l={saveApiKey(e){localStorage.setItem(a.API_KEY,e)},getApiKey:()=>localStorage.getItem(a.API_KEY),async validateApiKey(e){try{let r=await s.Z.get("".concat(t.P.models.list),{headers:{Authorization:"Bearer ".concat(e)}});if(200===r.status)return this.saveApiKey(e),{success:!0};return{success:!1,error:"验证失败"}}catch(e){if(e instanceof i.a)return{success:!1,error:e.message||"API 密钥验证失败"};if(e instanceof o.d7){var r,n,a;if((null===(r=e.response)||void 0===r?void 0:r.status)===403)return{success:!1,error:"API 密钥无效或未授权访问"};return{success:!1,error:(null===(a=e.response)||void 0===a?void 0:null===(n=a.data)||void 0===n?void 0:n.message)||"API 密钥验证失败"}}return{success:!1,error:"验证过程中发生未知错误"}}},async logout(){localStorage.removeItem(a.API_KEY)}}},2825:function(e,r,n){"use strict";n.d(r,{a:function(){return t}});class t extends Error{static fromError(e){if(e.response){var r,n;return new t(e.response.status,(null===(r=e.response.data)||void 0===r?void 0:r.code)||"UNKNOWN_ERROR",(null===(n=e.response.data)||void 0===n?void 0:n.detail)||"未知错误")}return new t(0,"UNKNOWN_ERROR",e.message||"未知错误")}constructor(e,r,n){super(n),this.statusCode=e,this.code=r,this.name="ApiException"}}},9519:function(e){e.exports={"page-container":"PageContainer_page-container__F2_vX",mobile:"PageContainer_mobile__HDFuu","mobile-content":"PageContainer_mobile-content__QrMA_","page-container-content":"PageContainer_page-container-content__jmDFL"}}},function(e){e.O(0,[587,385,205,518,610,212,245,633,971,69,744],function(){return e(e.s=4313)}),_N_E=e.O()}]); -------------------------------------------------------------------------------- /static/_next/static/chunks/app/layout-396a030f32c8d339.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[185],{724:function(e,r,o){Promise.resolve().then(o.bind(o,6543)),Promise.resolve().then(o.bind(o,877)),Promise.resolve().then(o.t.bind(o,3385,23)),Promise.resolve().then(o.bind(o,4662)),Promise.resolve().then(o.bind(o,6138))},6543:function(e,r,o){"use strict";o.r(r),o.d(r,{ConfigProvider:function(){return n.ZP}});var n=o(3292)},4662:function(e,r,o){"use strict";o.r(r),o.d(r,{default:function(){return d}});var n=o(3827),a=o(2688),t=o(7082),i=o(4090);function d(e){let{children:r}=e,[o]=(0,i.useState)(()=>new a.S({defaultOptions:{queries:{retry:1,refetchOnWindowFocus:!1}}}));return(0,n.jsx)(t.aH,{client:o,children:r})}},6138:function(e,r,o){"use strict";o.r(r),o.d(r,{default:function(){return l}});var n=o(3827),a=o(4090),t=o(3292),i=o(8405),d=o(4848);let s={token:{colorPrimary:"#1677ff",colorSuccess:"#52c41a",colorWarning:"#faad14",colorError:"#ff4d4f",colorInfo:"#1677ff",colorBgBase:"#ffffff",colorTextBase:"#000000",borderRadius:6,wireframe:!1,fontFamily:'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',fontSize:14,boxShadow:"0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px 0 rgba(0, 0, 0, 0.02)",boxShadowSecondary:"0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05)"},components:{Menu:{itemHeight:40,itemMarginInline:12,itemMarginBlock:4,iconMarginInlineEnd:16,collapsedIconSize:16},Layout:{bodyBg:"#f5f5f5",headerBg:"#ffffff",headerHeight:64,siderBg:"#ffffff"},Card:{headerBg:"transparent",borderRadiusLG:8},Table:{headerBg:"#fafafa",borderRadiusLG:8},Form:{itemMarginBottom:24},Button:{paddingInlineLG:16,borderRadiusLG:6}}},c={token:{colorPrimary:"#177ddc",colorSuccess:"#49aa19",colorWarning:"#d89614",colorError:"#a61d24",colorInfo:"#177ddc",colorBgBase:"#141414",colorTextBase:"rgba(255, 255, 255, 0.85)",borderRadius:6,wireframe:!1,fontFamily:'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',fontSize:14,boxShadow:"0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px 0 rgba(0, 0, 0, 0.02)",boxShadowSecondary:"0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05)"},components:{Menu:{itemHeight:40,itemMarginInline:12,itemMarginBlock:4,iconMarginInlineEnd:16,collapsedIconSize:16,darkItemBg:"#141414",darkSubMenuItemBg:"#141414",darkItemColor:"rgba(255, 255, 255, 0.65)",darkItemSelectedColor:"#177ddc",darkItemSelectedBg:"rgba(0, 0, 0, 0.16)",darkItemHoverBg:"rgba(255, 255, 255, 0.08)"},Layout:{bodyBg:"#000000",headerBg:"#141414",headerHeight:64,siderBg:"#141414"},Card:{headerBg:"transparent",borderRadiusLG:8},Table:{headerBg:"#1d1d1d",borderRadiusLG:8},Form:{itemMarginBottom:24},Button:{paddingInlineLG:16,borderRadiusLG:6}}};function f(e){let{children:r}=e,[o,f]=(0,a.useState)(!1),{resolvedTheme:l}=(0,d.F)(),[u,g]=(0,a.useState)(s);return((0,a.useEffect)(()=>{f(!0)},[]),(0,a.useEffect)(()=>{o&&g("dark"===l?c:s)},[l,o]),o)?(0,n.jsx)(t.ZP,{theme:u,children:(0,n.jsx)(i.Z,{children:r})}):null}function l(e){let{children:r}=e;return(0,n.jsx)(d.f,{attribute:"class",defaultTheme:"light",enableSystem:!0,children:(0,n.jsx)(f,{children:r})})}},3385:function(){}},function(e){e.O(0,[587,385,245,95,971,69,744],function(){return e(e.s=724)}),_N_E=e.O()}]); -------------------------------------------------------------------------------- /static/_next/static/chunks/app/page-70327e014d79b0e2.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[931],{2061:function(n,e,t){Promise.resolve().then(t.bind(t,8167)),Promise.resolve().then(t.t.bind(t,5250,23))},8167:function(n,e,t){"use strict";t.r(e),t.d(e,{Button:function(){return u.ZP}});var u=t(1587)}},function(n){n.O(0,[587,250,971,69,744],function(){return n(n.s=2061)}),_N_E=n.O()}]); -------------------------------------------------------------------------------- /static/_next/static/chunks/main-app-2552f5e9a5d47588.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[744],{9285:function(e,n,t){Promise.resolve().then(t.t.bind(t,7690,23)),Promise.resolve().then(t.t.bind(t,8955,23)),Promise.resolve().then(t.t.bind(t,5613,23)),Promise.resolve().then(t.t.bind(t,1902,23)),Promise.resolve().then(t.t.bind(t,1778,23)),Promise.resolve().then(t.t.bind(t,7831,23))}},function(e){var n=function(n){return e(e.s=n)};e.O(0,[971,69],function(){return n(5317),n(9285)}),_N_E=e.O()}]); -------------------------------------------------------------------------------- /static/_next/static/chunks/pages/_app-75f6107b0260711c.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[888],{1597:function(n,_,u){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_app",function(){return u(7174)}])}},function(n){var _=function(_){return n(n.s=_)};n.O(0,[774,179],function(){return _(1597),_(4546)}),_N_E=n.O()}]); -------------------------------------------------------------------------------- /static/_next/static/chunks/pages/_error-9a890acb1e81c3fc.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[820],{1981:function(n,_,u){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_error",function(){return u(5103)}])}},function(n){n.O(0,[888,774,179],function(){return n(n.s=1981)}),_N_E=n.O()}]); -------------------------------------------------------------------------------- /static/_next/static/chunks/webpack-909dbc96a8209865.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";var e,t,n,r,o,u,i,c,f,a={},l={};function s(e){var t=l[e];if(void 0!==t)return t.exports;var n=l[e]={exports:{}},r=!0;try{a[e](n,n.exports,s),r=!1}finally{r&&delete l[e]}return n.exports}s.m=a,e=[],s.O=function(t,n,r,o){if(n){o=o||0;for(var u=e.length;u>0&&e[u-1][2]>o;u--)e[u]=e[u-1];e[u]=[n,r,o];return}for(var i=1/0,u=0;u=o&&Object.keys(s.O).every(function(e){return s.O[e](n[f])})?n.splice(f--,1):(c=!1,oQwen2API Frontend -------------------------------------------------------------------------------- /static/index.txt: -------------------------------------------------------------------------------- 1 | 2:I[5250,["587","static/chunks/587-ddfc51f5a9d66e65.js","250","static/chunks/250-1326d26798efe2a8.js","931","static/chunks/app/page-70327e014d79b0e2.js"],""] 2 | 3:I[8167,["587","static/chunks/587-ddfc51f5a9d66e65.js","250","static/chunks/250-1326d26798efe2a8.js","931","static/chunks/app/page-70327e014d79b0e2.js"],"Button"] 3 | 4:I[877,["587","static/chunks/587-ddfc51f5a9d66e65.js","385","static/chunks/385-76a53e116716a735.js","245","static/chunks/245-836a16859841a26d.js","95","static/chunks/95-06814ef9f06ba32c.js","185","static/chunks/app/layout-396a030f32c8d339.js"],""] 4 | 5:I[4662,["587","static/chunks/587-ddfc51f5a9d66e65.js","385","static/chunks/385-76a53e116716a735.js","245","static/chunks/245-836a16859841a26d.js","95","static/chunks/95-06814ef9f06ba32c.js","185","static/chunks/app/layout-396a030f32c8d339.js"],""] 5 | 6:I[6138,["587","static/chunks/587-ddfc51f5a9d66e65.js","385","static/chunks/385-76a53e116716a735.js","245","static/chunks/245-836a16859841a26d.js","95","static/chunks/95-06814ef9f06ba32c.js","185","static/chunks/app/layout-396a030f32c8d339.js"],""] 6 | 7:I[6543,["587","static/chunks/587-ddfc51f5a9d66e65.js","385","static/chunks/385-76a53e116716a735.js","245","static/chunks/245-836a16859841a26d.js","95","static/chunks/95-06814ef9f06ba32c.js","185","static/chunks/app/layout-396a030f32c8d339.js"],"ConfigProvider"] 7 | 11:I[5613,[],""] 8 | 12:I[1778,[],""] 9 | 8:["开始时间","结束时间"] 10 | b:["开始日期","结束日期"] 11 | c:["开始年份","结束年份"] 12 | d:["开始月份","结束月份"] 13 | e:["开始季度","结束季度"] 14 | f:["开始周","结束周"] 15 | a:{"placeholder":"请选择日期","yearPlaceholder":"请选择年份","quarterPlaceholder":"请选择季度","monthPlaceholder":"请选择月份","weekPlaceholder":"请选择周","rangePlaceholder":"$b","rangeYearPlaceholder":"$c","rangeMonthPlaceholder":"$d","rangeQuarterPlaceholder":"$e","rangeWeekPlaceholder":"$f","yearFormat":"YYYY年","dayFormat":"D","cellMeridiemFormat":"A","monthBeforeYear":false,"locale":"zh_CN","today":"今天","now":"此刻","backToToday":"返回今天","ok":"确定","timeSelect":"选择时间","dateSelect":"选择日期","weekSelect":"选择周","clear":"清除","week":"周","month":"月","year":"年","previousMonth":"上个月 (翻页上键)","nextMonth":"下个月 (翻页下键)","monthSelect":"选择月份","yearSelect":"选择年份","decadeSelect":"选择年代","previousYear":"上一年 (Control键加左方向键)","nextYear":"下一年 (Control键加右方向键)","previousDecade":"上一年代","nextDecade":"下一年代","previousCentury":"上一世纪","nextCentury":"下一世纪","cellDateFormat":"D"} 16 | 10:{"placeholder":"请选择时间","rangePlaceholder":"$8"} 17 | 9:{"lang":"$a","timePickerLocale":"$10"} 18 | 0:["BXR8W7u2Bte2AoAKInO7B",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},["$L1",["$","main",null,{"className":"flex min-h-screen flex-col items-center justify-center p-24","children":["$","div",null,{"className":"text-center","children":[["$","h1",null,{"className":"text-4xl font-bold mb-8","children":"Qwen2API"}],["$","p",null,{"className":"text-lg mb-8","children":"欢迎使用 Qwen2API,请登录以继续。"}],["$","$L2",null,{"href":"/account/login","children":["$","$L3",null,{"type":"primary","size":"large","children":"立即登录"}]}]]}]}],null]]},[null,["$","html",null,{"lang":"zh-CN","className":"h-full","children":["$","body",null,{"className":"h-full m-0 p-0","children":["$","$L4",null,{"children":["$","$L5",null,{"children":["$","$L6",null,{"children":["$","$L7",null,{"theme":{"components":{"Tree":{"directoryNodeSelectedBg":"#E6F7FF"}}},"locale":{"locale":"zh-cn","Pagination":{"items_per_page":"条/页","jump_to":"跳至","jump_to_confirm":"确定","page":"页","prev_page":"上一页","next_page":"下一页","prev_5":"向前 5 页","next_5":"向后 5 页","prev_3":"向前 3 页","next_3":"向后 3 页","page_size":"页码"},"DatePicker":{"lang":{"placeholder":"请选择日期","yearPlaceholder":"请选择年份","quarterPlaceholder":"请选择季度","monthPlaceholder":"请选择月份","weekPlaceholder":"请选择周","rangePlaceholder":["开始日期","结束日期"],"rangeYearPlaceholder":["开始年份","结束年份"],"rangeMonthPlaceholder":["开始月份","结束月份"],"rangeQuarterPlaceholder":["开始季度","结束季度"],"rangeWeekPlaceholder":["开始周","结束周"],"yearFormat":"YYYY年","dayFormat":"D","cellMeridiemFormat":"A","monthBeforeYear":false,"locale":"zh_CN","today":"今天","now":"此刻","backToToday":"返回今天","ok":"确定","timeSelect":"选择时间","dateSelect":"选择日期","weekSelect":"选择周","clear":"清除","week":"周","month":"月","year":"年","previousMonth":"上个月 (翻页上键)","nextMonth":"下个月 (翻页下键)","monthSelect":"选择月份","yearSelect":"选择年份","decadeSelect":"选择年代","previousYear":"上一年 (Control键加左方向键)","nextYear":"下一年 (Control键加右方向键)","previousDecade":"上一年代","nextDecade":"下一年代","previousCentury":"上一世纪","nextCentury":"下一世纪","cellDateFormat":"D"},"timePickerLocale":{"placeholder":"请选择时间","rangePlaceholder":["开始时间","结束时间"]}},"TimePicker":{"placeholder":"请选择时间","rangePlaceholder":"$8"},"Calendar":"$9","global":{"placeholder":"请选择"},"Table":{"filterTitle":"筛选","filterConfirm":"确定","filterReset":"重置","filterEmptyText":"无筛选项","filterCheckAll":"全选","filterSearchPlaceholder":"在筛选项中搜索","emptyText":"暂无数据","selectAll":"全选当页","selectInvert":"反选当页","selectNone":"清空所有","selectionAll":"全选所有","sortTitle":"排序","expand":"展开行","collapse":"关闭行","triggerDesc":"点击降序","triggerAsc":"点击升序","cancelSort":"取消排序"},"Modal":{"okText":"确定","cancelText":"取消","justOkText":"知道了"},"Tour":{"Next":"下一步","Previous":"上一步","Finish":"结束导览"},"Popconfirm":{"cancelText":"取消","okText":"确定"},"Transfer":{"titles":["",""],"searchPlaceholder":"请输入搜索内容","itemUnit":"项","itemsUnit":"项","remove":"删除","selectCurrent":"全选当页","removeCurrent":"删除当页","selectAll":"全选所有","deselectAll":"取消全选","removeAll":"删除全部","selectInvert":"反选当页"},"Upload":{"uploading":"文件上传中","removeFile":"删除文件","uploadError":"上传错误","previewFile":"预览文件","downloadFile":"下载文件"},"Empty":{"description":"暂无数据"},"Icon":{"icon":"图标"},"Text":{"edit":"编辑","copy":"复制","copied":"复制成功","expand":"展开","collapse":"收起"},"Form":{"optional":"(可选)","defaultValidateMessages":{"default":"字段验证错误${label}","required":"请输入${label}","enum":"$${label}必须是其中一个[${enum}]","whitespace":"$${label}不能为空字符","date":{"format":"$${label}日期格式无效","parse":"$${label}不能转换为日期","invalid":"$${label}是一个无效日期"},"types":{"string":"$${label}不是一个有效的${type}","method":"$${label}不是一个有效的${type}","array":"$${label}不是一个有效的${type}","object":"$${label}不是一个有效的${type}","number":"$${label}不是一个有效的${type}","date":"$${label}不是一个有效的${type}","boolean":"$${label}不是一个有效的${type}","integer":"$${label}不是一个有效的${type}","float":"$${label}不是一个有效的${type}","regexp":"$${label}不是一个有效的${type}","email":"$${label}不是一个有效的${type}","url":"$${label}不是一个有效的${type}","hex":"$${label}不是一个有效的${type}"},"string":{"len":"$${label}须为${len}个字符","min":"$${label}最少${min}个字符","max":"$${label}最多${max}个字符","range":"$${label}须在${min}-${max}字符之间"},"number":{"len":"$${label}必须等于${len}","min":"$${label}最小值为${min}","max":"$${label}最大值为${max}","range":"$${label}须在${min}-${max}之间"},"array":{"len":"须为${len}个${label}","min":"最少${min}个${label}","max":"最多${max}个${label}","range":"$${label}数量须在${min}-${max}之间"},"pattern":{"mismatch":"$${label}与模式不匹配${pattern}"}}},"Image":{"preview":"预览"},"QRCode":{"expired":"二维码过期","refresh":"点击刷新","scanned":"已扫描"},"ColorPicker":{"presetEmpty":"暂无","transparent":"无色","singleColor":"单色","gradientColor":"渐变色"}},"children":["$","$L11",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","loadingScripts":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L12",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[],"styles":null}]}]}]}]}]}]}],null]],[[["$","link","0",{"rel":"stylesheet","href":"/static/_next/static/css/287f887716c32fb8.css","precedence":"next","crossOrigin":""}]],"$L13"]]]] 19 | 13:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"Qwen2API Frontend"}],["$","meta","3",{"name":"description","content":"Qwen2API Frontend Application by fengwind"}]] 20 | 1:null 21 | --------------------------------------------------------------------------------