├── .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""
238 | },
239 | 'finish_reason': 'stop'
240 | }
241 | ],
242 | 'usage': {
243 | 'prompt_tokens': len(content), # 使用content长度作为prompt_tokens
244 | 'completion_tokens': len(f""), # 使用生成的markdown长度作为completion_tokens
245 | 'total_tokens': len(content) + len(f"") # 总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 |
--------------------------------------------------------------------------------