sanbo commited on
Commit
f118c0c
·
1 Parent(s): 509a621

update sth. at 2025-01-26 00:51:18

Browse files
Files changed (8) hide show
  1. .dockerignore +128 -0
  2. .gitignore +79 -0
  3. Dockerfile +48 -1
  4. base_chat_format.py +30 -0
  5. base_get_channel.py +39 -0
  6. duckai_service.py +543 -0
  7. more_core.py +327 -0
  8. requirements.txt +13 -0
.dockerignore ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Version Control Systems
2
+ .git/
3
+ .gitignore
4
+ .gitattributes
5
+ .svn/
6
+ .hg/
7
+ .github/
8
+
9
+ # Environment and Configuration
10
+ .env
11
+ .env.*
12
+ *.env
13
+ .venv/
14
+ venv/
15
+ env/
16
+ ENV/
17
+ .python-version
18
+ pip-selfcheck.json
19
+ requirements.txt.bak
20
+
21
+ # Python Specific
22
+ __pycache__/
23
+ *.py[cod]
24
+ *$py.class
25
+ *.so
26
+ .Python
27
+ build/
28
+ develop-eggs/
29
+ dist/
30
+ downloads/
31
+ eggs/
32
+ .eggs/
33
+ lib/
34
+ lib64/
35
+ parts/
36
+ sdist/
37
+ var/
38
+ wheels/
39
+ *.egg-info/
40
+ .installed.cfg
41
+ *.egg
42
+ MANIFEST
43
+
44
+ # IDE and Editors
45
+ .idea/
46
+ .vscode/
47
+ *.swp
48
+ *.swo
49
+ *~
50
+ .DS_Store
51
+ .settings/
52
+ .project
53
+ .pydevproject
54
+ .classpath
55
+ *.sublime-project
56
+ *.sublime-workspace
57
+ .editorconfig
58
+
59
+ # Testing and Documentation
60
+ .tox/
61
+ .coverage
62
+ .coverage.*
63
+ htmlcov/
64
+ .pytest_cache/
65
+ nosetests.xml
66
+ coverage.xml
67
+ *.cover
68
+ .hypothesis/
69
+ docs/
70
+ *.md
71
+ LICENSE*
72
+ README*
73
+
74
+ # Logs and Data
75
+ *.log
76
+ *.sql
77
+ *.sqlite
78
+ *.db
79
+ logs/
80
+ log/
81
+ data/
82
+ tmp/
83
+
84
+ # Cache and Temporary Files
85
+ .cache/
86
+ .mypy_cache/
87
+ .dmypy.json
88
+ dmypy.json
89
+ *.bak
90
+ *.tmp
91
+ *.temp
92
+ .*.swp
93
+ *.out
94
+
95
+ # Build and Deployment
96
+ .dockerignore
97
+ Dockerfile*
98
+ docker-compose*
99
+ *.yml
100
+ *.yaml
101
+ .gitlab-ci.yml
102
+ .travis.yml
103
+ .circleci/
104
+
105
+ # Dependencies and Packages
106
+ node_modules/
107
+ package-lock.json
108
+ yarn.lock
109
+ *.pyc
110
+ *.pyo
111
+ *.pyd
112
+ *.so
113
+ *.dylib
114
+ *.dll
115
+
116
+ # System Files
117
+ .DS_Store
118
+ Thumbs.db
119
+ desktop.ini
120
+ *.swp
121
+ *~
122
+
123
+ # Project Specific Backups
124
+ *_backup/
125
+ *_bak/
126
+ *.old
127
+ *.orig
128
+ *.rej
.gitignore ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .externalNativeBuild
2
+ import-summary.txt
3
+
4
+ #java files
5
+ *.class
6
+ *.dex
7
+ .sync/
8
+
9
+ #for idea temp file
10
+ *.iws
11
+ *.ipr
12
+ *.iml
13
+ target/
14
+ .idea/
15
+ .idea
16
+ .gradle/
17
+ release/
18
+ build/
19
+ spoon/
20
+ releasebak/
21
+
22
+ #mac temp file
23
+ __MACOSX
24
+ .DS_Store
25
+ ._.DS_Store
26
+
27
+ #for eclipse
28
+ .settings/
29
+ local.properties
30
+ *gen/
31
+ *.classpath
32
+ */bin/
33
+ bin/
34
+ .project
35
+
36
+ #temp file
37
+ *.bak
38
+
39
+ *.pmd
40
+ sh.exe.stackdump
41
+
42
+ .vs/
43
+ .vscode/
44
+
45
+ *.log
46
+ *.ctxt
47
+ .mtj.tmp/
48
+
49
+ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
50
+ hs_err_pid*
51
+
52
+ # Package Files #
53
+ # *.jar
54
+ *.war
55
+ *.nar
56
+ *.ear
57
+ *.zip
58
+ *.tar.gz
59
+ *.rar
60
+ *.cxx
61
+ *.cfg
62
+ # for nodejs
63
+ node_modules/
64
+ # for python
65
+ package-lock.json
66
+ .$*
67
+ *.drawio.bkp
68
+ __pycache__
69
+ *.pyc
70
+ *.pyo
71
+ *.pyd
72
+ .Python
73
+ env
74
+ .env
75
+ .venv
76
+ pip-log.txt
77
+ dev.md
78
+
79
+
Dockerfile CHANGED
@@ -1 +1,48 @@
1
- FROM ghcr.io/hhhaiai/pekingduck:latest
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 使用官方Python镜像
2
+ FROM python:3.11-slim
3
+
4
+ # 设置工作目录
5
+ WORKDIR /app
6
+
7
+ # 安装系统依赖和字体
8
+ RUN apt-get update && apt-get install -y \
9
+ fonts-ipafont-gothic \
10
+ fonts-wqy-zenhei \
11
+ fonts-thai-tlwg \
12
+ fonts-kacst \
13
+ fonts-freefont-ttf \
14
+ libxss1 \
15
+ libnss3 \
16
+ libnspr4 \
17
+ libatk1.0-0 \
18
+ libatk-bridge2.0-0 \
19
+ libcups2 \
20
+ libdrm2 \
21
+ libxkbcommon0 \
22
+ libxcomposite1 \
23
+ libxdamage1 \
24
+ libxfixes3 \
25
+ libxrandr2 \
26
+ libgbm1 \
27
+ libasound2 \
28
+ && rm -rf /var/lib/apt/lists/*
29
+
30
+ # 复制项目文件
31
+ COPY . .
32
+
33
+ # 安装Python依赖
34
+ RUN pip install --no-cache-dir -r requirements.txt
35
+
36
+ # 安装Playwright及其依赖
37
+ RUN playwright install chromium --with-deps
38
+
39
+ # 环境变量配置
40
+ ENV PYTHONUNBUFFERED=1
41
+ ENV DEBUG=false
42
+ ENV PORT=7860
43
+
44
+ # 暴露端口
45
+ EXPOSE 7860
46
+
47
+ # 启动命令
48
+ CMD ["python", "more_core.py"]
base_chat_format.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import random
3
+ import string
4
+ def is_chatgpt_format(data):
5
+ """Check if the data is in the expected ChatGPT format"""
6
+ try:
7
+ # If the data is a string, try to parse it as JSON
8
+ if isinstance(data, str):
9
+ try:
10
+ data = json.loads(data)
11
+ except json.JSONDecodeError:
12
+ return False # If the string can't be parsed, it's not in the expected format
13
+
14
+ # Now check if data is a dictionary and contains the necessary structure
15
+ if isinstance(data, dict):
16
+ # Ensure 'choices' is a list and the first item has a 'message' field
17
+ if "choices" in data and isinstance(data["choices"], list) and len(data["choices"]) > 0:
18
+ if "message" in data["choices"][0]:
19
+ return True
20
+ except Exception as e:
21
+ print(f"Error checking ChatGPT format: {e}")
22
+
23
+ return False
24
+
25
+
26
+ def _generate_id(letters: int = 4, numbers: int = 6) -> str:
27
+ """Generate unique chat completion ID"""
28
+ letters_str = ''.join(random.choices(string.ascii_lowercase, k=letters))
29
+ numbers_str = ''.join(random.choices(string.digits, k=numbers))
30
+ return f"chatcmpl-{letters_str}{numbers_str}"
base_get_channel.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+
4
+ def get_channel_company(value: str, description: str = "") -> str:
5
+ """
6
+ 提取模型所属公司,优先级:value > description > 截取value首段
7
+ """
8
+
9
+ # 匹配规则表(可扩展)
10
+ RULES = [
11
+ (r"claude", "Anthropic"),
12
+ (r"gemini|palm", "Google"),
13
+ (r"llama2?|llama3?|meta-llama", "Meta"),
14
+ (r"gpt-|dall·e|o1|o2|o3|o4", "OpenAI"),
15
+ (r"deepseek", "DeepSeek"),
16
+ (r"abab|minimax", "MiniMax"),
17
+ (r"mistral", "Mistral"),
18
+ (r"ernie|文心一言", "Baidu"),
19
+ (r"chatglm|智谱", "Zhipu")
20
+ ]
21
+
22
+ def _match(text: str) -> str | None:
23
+ """从文本中匹配公司(兼容空值)"""
24
+ if not text: return None
25
+ return next((owner for pattern, owner in RULES if re.search(pattern, text.lower())), None)
26
+
27
+ # 优先级逻辑
28
+ owner = _match(value) or _match(description)
29
+ if owner: return owner
30
+
31
+ # 截取首段逻辑优化(过滤空字符)
32
+ if value:
33
+ # 分割并过滤空字符串
34
+ #parts = [p for p in re.split(r'[-\s/]+', value) if p.strip()]
35
+ parts = [p for p in re.split(r'[-\s/._]+', value) if p.strip()]
36
+ if parts:
37
+ return parts[0].capitalize() # 首字母大写
38
+
39
+ return "unknown" # 全空或无有效内容
duckai_service.py ADDED
@@ -0,0 +1,543 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ from playwright.sync_api import sync_playwright
3
+ import json
4
+ import base_get_channel as channel
5
+ from typing import Optional, Dict
6
+ import time
7
+ from datetime import datetime, timedelta
8
+ # 禁用 SSL 警告
9
+ import urllib3
10
+
11
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
12
+
13
+ debug = True
14
+ last_request_time = 0 # 上次请求的时间戳
15
+ cache_duration = 14400 # 缓存有效期,单位:秒 (4小时)
16
+ '''用于存储缓存的模型数据'''
17
+ cached_models = {
18
+ "object": "list",
19
+ "data": [],
20
+ "version": "1.0.0",
21
+ "provider": "DuckAI",
22
+ "name": "DuckAI",
23
+ "default_locale": "zh-CN",
24
+ "status": True,
25
+ "time": 0
26
+ }
27
+
28
+ '''基础模型'''
29
+ base_model = "gpt-4o-mini"
30
+ # 全局变量:存储所有模型的统计信息
31
+ # 格式:{model_name: {"calls": 调用次数, "fails": 失败次数, "last_fail": 最后失败时间}}
32
+ MODEL_STATS: Dict[str, Dict] = {}
33
+
34
+
35
+ def record_call(model_name: str, success: bool = True) -> None:
36
+ """
37
+ 记录模型调用情况
38
+ Args:
39
+ model_name: 模型名称
40
+ success: 调用是否成功
41
+ """
42
+ global MODEL_STATS
43
+ if model_name not in MODEL_STATS:
44
+ MODEL_STATS[model_name] = {"calls": 0, "fails": 0, "last_fail": None}
45
+
46
+ stats = MODEL_STATS[model_name]
47
+ stats["calls"] += 1
48
+ if not success:
49
+ stats["fails"] += 1
50
+ stats["last_fail"] = datetime.now()
51
+
52
+
53
+ def get_auto_model(cooldown_seconds: int = 300) -> str:
54
+ """异步获取最优模型"""
55
+ try:
56
+ if not MODEL_STATS:
57
+ get_models()
58
+
59
+ best_model = None
60
+ best_rate = -1.0
61
+ now = datetime.now()
62
+
63
+ for name, stats in MODEL_STATS.items():
64
+ if stats.get("last_fail") and (now - stats["last_fail"]) < timedelta(seconds=cooldown_seconds):
65
+ continue
66
+
67
+ total_calls = stats["calls"]
68
+ if total_calls > 0:
69
+ success_rate = (total_calls - stats["fails"]) / total_calls
70
+ if success_rate > best_rate:
71
+ best_rate = success_rate
72
+ best_model = name
73
+
74
+ default_model = best_model or base_model
75
+ if debug:
76
+ print(f"选择模型: {default_model}")
77
+ return default_model
78
+ except Exception as e:
79
+ if debug:
80
+ print(f"模型选择错误: {e}")
81
+ return base_model
82
+
83
+
84
+ def get_models():
85
+ """model data retrieval with thread safety"""
86
+ global cached_models, last_request_time
87
+ current_time = time.time()
88
+ if (current_time - last_request_time) > cache_duration:
89
+ try:
90
+ if debug:
91
+ print(f"will get model ")
92
+ # Update timestamp before awaiting to prevent concurrent updates
93
+ get_model_impl_by_playwright()
94
+ last_request_time = current_time
95
+ if debug:
96
+ print(f"success get model ")
97
+ except Exception as e:
98
+ print(f"000000---{e}")
99
+
100
+ return json.dumps(cached_models)
101
+
102
+
103
+ def get_model_impl_by_playwright():
104
+ global cached_models
105
+ """
106
+ 从网页获取获取模型
107
+ """
108
+ with sync_playwright() as p:
109
+ browser = p.chromium.launch(headless=True)
110
+ context = browser.new_context()
111
+ page = context.new_page()
112
+
113
+ try:
114
+ # 访问页面
115
+ page.goto("https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=1")
116
+
117
+ # 点击 Get Started 按钮
118
+ get_started_button_xpath = '//*[@id="react-layout"]/div/div[2]/main/div/div/div[2]/div/button'
119
+ page.wait_for_selector(get_started_button_xpath)
120
+ page.click(get_started_button_xpath)
121
+
122
+ # 等待所有模型加载完成
123
+ page.wait_for_function(
124
+ "document.querySelectorAll('ul[role=\"radiogroup\"] > li').length > 0"
125
+ )
126
+
127
+ # 解析模型信息
128
+ parser_models_info_form_page(page)
129
+
130
+ except Exception as e:
131
+ print(f"发生错误: {e}")
132
+ finally:
133
+ browser.close()
134
+
135
+ return cached_models
136
+
137
+
138
+ def parser_models_info_form_page(page):
139
+ global cached_models
140
+ models = page.query_selector_all('ul[role="radiogroup"] > li')
141
+
142
+ # 创建现有模型的 ID 集合用于快速查找
143
+ existing_ids = {item["id"] for item in cached_models['data']}
144
+ result = []
145
+ # 确保有内容时更新
146
+ is_update = False
147
+ for model in models:
148
+ # 使用更精确的选择器
149
+ name_element = model.query_selector('.J58ouJfofMIxA2Ukt6lA')
150
+ description_element = model.query_selector('.tDjqHxDUIeGL37tpvoSI')
151
+ # 模型真实名字
152
+ value = model.query_selector('input').get_attribute('value')
153
+ # 确保有效
154
+ if not name_element or not value:
155
+ continue
156
+
157
+ # 模型描述
158
+ name = name_element.inner_text()
159
+ description = description_element.inner_text() if description_element else ""
160
+ # 确定描述供应商
161
+ owned_by = channel.get_channel_company(value, description)
162
+ # 生成新模型数据
163
+ new_model = {
164
+ "id": value,
165
+ "object": "model",
166
+ "_type": "text",
167
+ "created": int(time.time() * 1000), # 使用当前时间戳
168
+ "owned_by": owned_by,
169
+ "description": name
170
+ }
171
+ # 记录成功
172
+ record_call(value)
173
+ # 检查是否已存在相同 ID 的模型
174
+ if new_model['id'] in existing_ids:
175
+ # 更新已存在的模型数据
176
+ for idx, item in enumerate(cached_models['data']):
177
+ if item['id'] == new_model['id']:
178
+ cached_models['data'][idx] = new_model # 完全替换旧数据
179
+ break
180
+ else:
181
+ # 添加新模型到缓存
182
+ cached_models['data'].append(new_model)
183
+
184
+ is_updated = True
185
+
186
+ # 仅在检测到更新时刷新时间戳
187
+ if is_updated:
188
+ cached_models['time'] = int(time.time() * 1000)
189
+ return json.dumps(cached_models, ensure_ascii=False)
190
+
191
+
192
+ def is_model_available(model_id: str, cooldown_seconds: int = 300) -> bool:
193
+ """
194
+ 判断模型是否在模型列表中且非最近失败的模型
195
+
196
+ Args:
197
+ model_id: 模型ID,需要检查的模型标识符
198
+ cooldown_seconds: 失败冷却时间(秒),默认300秒
199
+
200
+ Returns:
201
+ bool: 如果模型可用返回True,否则返回False
202
+
203
+ Note:
204
+ - 当MODEL_STATS为空时会自动调用get_models()更新数据
205
+ - 检查模型是否在冷却期内,如果在冷却期则返回False
206
+ """
207
+ global MODEL_STATS
208
+
209
+ # 如果MODEL_STATS为空,加载模型数据
210
+ if not MODEL_STATS:
211
+ get_models()
212
+
213
+ # 检查模型是否在统计信息中
214
+ if model_id not in MODEL_STATS:
215
+ return False
216
+
217
+ # 检查是否在冷却期内
218
+ stats = MODEL_STATS[model_id]
219
+ if stats["last_fail"]:
220
+ time_since_failure = datetime.now() - stats["last_fail"]
221
+ if time_since_failure < timedelta(seconds=cooldown_seconds):
222
+ return False
223
+
224
+ return True
225
+
226
+
227
+ def get_model_by_autoupdate(model_id: Optional[str] = None, cooldown_seconds: int = 300) -> Optional[str]:
228
+ """
229
+ 检查提供的model_id是否可用,如果不可用则返回成功率最高的模型
230
+
231
+ Args:
232
+ model_id: 指定的模型ID,可选参数
233
+ cooldown_seconds: 失败冷却时间(秒),默认300秒
234
+
235
+ Returns:
236
+ str | None: 返回可用的模型ID,如果没有可用模型则返回None
237
+
238
+ Note:
239
+ - 当MODEL_STATS为空时会自动调用get_models()更新数据
240
+ - 如果指定的model_id可用,则直接返回
241
+ - 如果指定的model_id不可用,则返回成功率最高的模型
242
+ """
243
+ global MODEL_STATS
244
+
245
+ # 如果MODEL_STATS为空,加载模型数据
246
+ if not MODEL_STATS:
247
+ get_models()
248
+
249
+ # 如果提供了model_id且可用,直接返回
250
+ if model_id and is_model_available(model_id, cooldown_seconds):
251
+ return model_id
252
+
253
+ # 否则返回成功率最高的可用模型
254
+ return get_auto_model(cooldown_seconds=cooldown_seconds)
255
+
256
+
257
+ ################################################################################################
258
+
259
+
260
+ # 元素存放变量及时间
261
+ vqd4_time = ("", 0)
262
+
263
+ def extract_x_vqd_4(default_host='duckduckgo.com', max_retries=3, retry_delay=1):
264
+ """
265
+ 获取 x-vqd-4 token
266
+ Args:
267
+ default_host: 请求的主机地址
268
+ max_retries: 最大重试次数
269
+ retry_delay: 重试延迟时间(秒)
270
+ Returns:
271
+ str: 成功返回token,失败返回空字符串
272
+ """
273
+ url = f"https://{default_host}/duckchat/v1/status"
274
+ global vqd4_time
275
+ headers = {
276
+ 'Accept': '*/*', # 修正 Accept 头
277
+ 'Accept-Encoding': 'gzip, deflate, br',
278
+ 'Accept-Language': 'zh-CN,zh-Hans;q=0.9',
279
+ 'Cache-Control': 'no-store', # 修正缓存控制
280
+ 'Connection': 'keep-alive',
281
+ 'Host': default_host,
282
+ 'Pragma': 'no-cache',
283
+ 'Referer': f"https://{default_host}/",
284
+ 'Sec-Fetch-Dest': 'empty',
285
+ 'Sec-Fetch-Mode': 'cors',
286
+ 'Sec-Fetch-Site': 'same-origin',
287
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15 Ddg/17.4.1',
288
+ 'X-DuckDuckGo-Client': 'macOS',
289
+ 'x-vqd-accept': '1'
290
+ }
291
+
292
+ for attempt in range(max_retries):
293
+ try:
294
+ response = requests.get(
295
+ url,
296
+ headers=headers,
297
+ timeout=10, # 添加超时设置
298
+ verify=False # 忽略 SSL 验证
299
+ )
300
+ response.encoding = 'utf-8'
301
+
302
+ if response.status_code == 200:
303
+ if 'x-vqd-4' in response.headers:
304
+ vqd4 = response.headers['x-vqd-4']
305
+ if vqd4.strip(): # 验证token不为空
306
+ vqd4_time = (vqd4, int(time.time() * 1000))
307
+ if debug:
308
+ print(f"成功获取token: {vqd4}")
309
+ return vqd4
310
+
311
+ if debug:
312
+ print(f"Token为空,尝试次数: {attempt + 1}/{max_retries}")
313
+ else:
314
+ if debug:
315
+ print(f"请求失败,状态码: {response.status_code},尝试次数: {attempt + 1}/{max_retries}")
316
+
317
+ # 如果不是最后一次尝试,则等待后重试
318
+ if attempt < max_retries - 1:
319
+ time.sleep(retry_delay)
320
+
321
+ except requests.RequestException as e:
322
+ if debug:
323
+ print(f"请求异常: {e},尝试次数: {attempt + 1}/{max_retries}")
324
+ if attempt < max_retries - 1:
325
+ time.sleep(retry_delay)
326
+ continue
327
+
328
+ if debug:
329
+ print("获取token失败,已达到最大重试次数")
330
+ return "" # 所有重试都失败后返回空字符串
331
+
332
+ # def makesure_token():
333
+ # """
334
+ # 确保获取token:
335
+ # 1. 如内存没有,则新建
336
+ # 2. 内存键值对有,若失效,更新
337
+ # 2. 内存键值对有,且无失效,直接使用
338
+ # """
339
+ # global vqd4_time
340
+ # x_vqd_4_value = ''
341
+ # if not vqd4_time[0].strip():
342
+ # # 字符串为空或仅包含空格
343
+ # x_vqd_4_value = extract_x_vqd_4()
344
+ # else:
345
+ # # 字符串非空
346
+ # print("字符串非空")
347
+ # t = vqd4_time[1]
348
+ # # 获取当前的 UNIX 时间戳(以毫秒为单位)
349
+ # current_time = int(time.time() * 1000)
350
+ # # 计算时间差(绝对值)
351
+ # time_difference = abs(current_time - t)
352
+
353
+ # # 检查时间差是否大于 2 分钟 (120,000 毫秒)
354
+ # if time_difference > 2 * 60 * 1000:
355
+ # print("时间差大于 2 分钟")
356
+ # x_vqd_4_value = extract_x_vqd_4()
357
+ # else:
358
+ # print("时间差在 2 分钟以内")
359
+ # x_vqd_4_value = vqd4_time[0] # 直接使用缓存的 x_vqd_4
360
+
361
+ # # 确保有值
362
+ # if not x_vqd_4_value.strip():
363
+ # x_vqd_4_value = extract_x_vqd_4()
364
+ # return x_vqd_4_value
365
+
366
+
367
+ def parse_response(response_text):
368
+ """
369
+ 逐行解析
370
+ """
371
+ lines = response_text.split('\n')
372
+ result = ""
373
+ for line in lines:
374
+ if line.startswith("data:"):
375
+ data = json.loads(line[len("data:"):])
376
+ if "message" in data:
377
+ result += data["message"]
378
+ print(result)
379
+
380
+
381
+ def chat_completion_message(user_prompt, x_vqd_4='', model=base_model,
382
+ system_message='You are a helpful assistant.',
383
+ user_id: str = None, session_id: str = None, default_host="duckduckgo.com"
384
+ , stream=False, temperature=0.3, max_tokens=1024, top_p=0.5, frequency_penalty=0,
385
+ presence_penalty=0):
386
+ """
387
+ 单条消息请求: https://duckduckgo.com/duckchat/v1/chat
388
+ """
389
+
390
+ messages = [
391
+ # 需要 system-> user
392
+ {"role": "user", "content": system_message},
393
+ {"role": "user", "content": user_prompt}
394
+ ]
395
+ return chat_completion_messages(messages=messages, x_vqd_4=x_vqd_4, model=model, default_host=default_host
396
+ , user_id=user_id
397
+ , session_id=session_id
398
+ , stream=stream
399
+ , temperature=temperature
400
+ , max_tokens=max_tokens
401
+ , top_p=top_p
402
+ , frequency_penalty=frequency_penalty
403
+ , presence_penalty=presence_penalty
404
+ )
405
+
406
+
407
+ def chat_completion_messages(
408
+ messages,
409
+ x_vqd_4='',
410
+ model=base_model,
411
+ user_id: str = None,
412
+ session_id: str = None,
413
+ default_host="duckduckgo.com",
414
+ stream=False, temperature=0.3, max_tokens=1024, top_p=0.5,
415
+ frequency_penalty=0, presence_penalty=0):
416
+ try:
417
+ # 确保model有效
418
+ if not model or model == "auto":
419
+ model = get_auto_model()
420
+ else:
421
+ model = get_model_by_autoupdate(model)
422
+ if debug:
423
+ print(f"校准后的model: {model}")
424
+
425
+ # 处理 token
426
+ if x_vqd_4 is None or not x_vqd_4.strip():
427
+ x_vqd_4 = extract_x_vqd_4() # 使用 makesure_token 确保获取有效的 token
428
+
429
+ # # 验证 token 格式
430
+ # if not x_vqd_4.strip() or x_vqd_4.startswith("x-vqd-4 parameter not found"):
431
+ # x_vqd_4 = makesure_token() # token 无效时重新获取
432
+
433
+ print(f"send_request 获取的token: {x_vqd_4}")
434
+
435
+ headers = {
436
+ 'Accept': 'text/event-stream',
437
+ 'Accept-Encoding': 'gzip, deflate, br',
438
+ 'Accept-Language': 'zh-CN,zh-Hans;q=0.9',
439
+ 'Cache-Control': 'no-cache',
440
+ 'Connection': 'keep-alive',
441
+ # 'Content-Length': '',
442
+ 'Content-Type': 'application/json',
443
+ # 'Cookie': 'av=1; bf=1; dcm=3; dcs=1; n=1',
444
+ 'Host': default_host,
445
+ 'Origin': f"https://{default_host}",
446
+ 'Pragma': 'no-cache',
447
+ 'Referer': f"https://{default_host}/",
448
+ 'Sec-Fetch-Dest': 'empty',
449
+ 'Sec-Fetch-Mode': 'cors',
450
+ 'Sec-Fetch-Site': 'same-origin',
451
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15 Ddg/17.4.1',
452
+ 'X-DuckDuckGo-Client': 'macOS'
453
+ }
454
+ # chat 独有请求头
455
+ # header.Set("accept", "text/event-stream")
456
+ # header.Set("x-vqd-4", token)
457
+
458
+ # 检查模型,如果messages 包含 system那么修改为user
459
+ for message in messages:
460
+ if message.get("role") == "system":
461
+ message["role"] = "user"
462
+
463
+ if x_vqd_4:
464
+ headers['x-vqd-4'] = x_vqd_4
465
+ data = {
466
+ "model": model,
467
+ "messages": messages
468
+ }
469
+ return chat_completion(default_host=default_host, model=model, headers=headers, payload=data)
470
+
471
+ except Exception as e:
472
+ print(f"使用模型[{model}]发生了异常:", e)
473
+ return ""
474
+
475
+
476
+ def chat_completion(default_host, model, headers, payload):
477
+ global vqd4_time
478
+ try:
479
+ response = requests.post(f'https://{default_host}/duckchat/v1/chat', headers=headers, json=payload)
480
+ response.encoding = 'utf-8' # 明确设置字符编码
481
+ response.raise_for_status()
482
+
483
+ # print("Status Code:", response.status_code)
484
+ # print("Content-Type:", response.headers.get('Content-Type'))
485
+ # print(response.text)
486
+ # print(response.headers)
487
+
488
+ final_content = ""
489
+ if response.status_code == 200:
490
+ # # 将 headers 转换为普通字典
491
+ # headers_dict = dict(response.headers)
492
+ # # 将字典转换为 JSON 字符串
493
+ # headers_json = json.dumps(headers_dict, ensure_ascii=False, indent=4)
494
+ # print(headers_json)
495
+ # 解析请求头
496
+ if 'x-vqd-4' in response.headers:
497
+ vqd4 = response.headers['x-vqd-4']
498
+ vqd4_time = (vqd4, int(time.time() * 1000))
499
+ # 解析响应内容
500
+ for line in response.iter_lines(decode_unicode=True):
501
+ # print(line)
502
+ # 检查 if 'data: [DONE]'在行中进行下一步动作
503
+ if 'data: [DONE]' in line:
504
+ # 如果找到结束信号,退出循环
505
+ break
506
+ elif line.startswith('data: '): # 确保行以'data: '开头
507
+ data_json = line[6:] # 删除行前缀'data: '
508
+ datax = json.loads(data_json) # 解析JSON字符串为字典
509
+ if 'message' in datax:
510
+ final_content += datax['message']
511
+ # print( final_content)
512
+ # 保存最终的content结果
513
+ final_result = final_content
514
+ return final_result
515
+ except Exception as e:
516
+ print(f"使用模型[{model}]发生了异常:", e)
517
+
518
+
519
+ # 测试代码
520
+ if __name__ == "__main__":
521
+ time1 = time.time() * 1000
522
+ result_json = get_models()
523
+ time2 = time.time() * 1000
524
+ print(f"耗时: {time2 - time1}")
525
+ print(result_json)
526
+ result_json2 = get_models()
527
+ time3 = time.time() * 1000
528
+ print(f"耗时2: {time3 - time2}")
529
+ print(result_json2)
530
+
531
+ print(f"获取自动模型1:{get_model_by_autoupdate('hello')}")
532
+ print(f"获取自动模型2:{get_auto_model()}")
533
+
534
+
535
+ t1 = time.time()
536
+ res = chat_completion_message("你是谁?你使用的是什么模型?你的知识库截止到什么时间? ")
537
+ t2 = time.time()
538
+ print(
539
+ f"====================================={base_model} --->测试结果【{t2 - t1}】=====================================\r\n{res}\r\n")
540
+ res2 = chat_completion_message("你比较擅长什么技能? ")
541
+ t3 = time.time()
542
+ print(
543
+ f"====================================={base_model} --->测试结果【{t3 - t2}】=====================================\r\n{res2}\r\n")
more_core.py ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import multiprocessing
3
+ import os
4
+ import time
5
+ from typing import Dict, Any, List
6
+
7
+ import tiktoken
8
+ import uvicorn
9
+ from apscheduler.schedulers.background import BackgroundScheduler
10
+ from fastapi import FastAPI, Request, HTTPException
11
+ from fastapi.responses import JSONResponse
12
+ from starlette.responses import HTMLResponse
13
+ # 禁用 SSL 警告
14
+ import urllib3
15
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
16
+
17
+ import duckai_service as dg
18
+ import base_chat_format as forts
19
+
20
+
21
+ # debug for Log
22
+ debug = True
23
+
24
+ app = FastAPI(
25
+ title="pekingduck",
26
+ description="Pekingduck is delious~",
27
+ version="1.0.0|2025.1.26"
28
+ )
29
+
30
+
31
+ class APIServer:
32
+ """High-performance API server implementation"""
33
+
34
+ def __init__(self, app: FastAPI):
35
+ self.app = app
36
+ self.encoding = tiktoken.get_encoding("cl100k_base")
37
+ self._setup_routes()
38
+ self._setup_scheduler()
39
+ dg.get_models()
40
+ def _setup_scheduler(self):
41
+ """ Schedule tasks to check and reload routes and models at regular intervals. """
42
+ self.scheduler = BackgroundScheduler()
43
+ # Scheduled Task 1: Check and reload routes every 30 seconds. Calls _reload_routes_if_needed method to check if routes need to be updated
44
+ self.scheduler.add_job(self._reload_routes_if_needed, 'interval', seconds=30)
45
+
46
+ # Scheduled Task 2: Reload models every 30 minutes (1800 seconds). This task will check and update the model data periodically
47
+ self.scheduler.add_job(self._reload_check, 'interval', seconds=60 * 30)
48
+ self.scheduler.start()
49
+ def _setup_routes(self) -> None:
50
+ """Initialize API routes"""
51
+ self.routes = """Initialize API routes"""
52
+
53
+ # Static routes with names for filtering
54
+ @self.app.get("/", name="root", include_in_schema=False)
55
+ def root():
56
+ return HTMLResponse(content="<h1>hello. It's home page.</h1>")
57
+
58
+ @self.app.get("/web", name="web")
59
+ def web():
60
+ return HTMLResponse(content="<h1>hello. It's web page.</h1>")
61
+
62
+ @self.app.get("/health", name="health")
63
+ def health():
64
+ return JSONResponse(content={"status": "working"})
65
+
66
+
67
+ @self.app.get("/v1/models", name="models")
68
+ def models():
69
+ if debug:
70
+ print("Fetching models...")
71
+ models_str = dg.get_models()
72
+ try:
73
+ models_json = json.loads(models_str)
74
+ return JSONResponse(content=models_json)
75
+ except json.JSONDecodeError as e:
76
+ raise HTTPException(status_code=500,
77
+ detail=f"Invalid models data: {str(e)}")
78
+
79
+ # Register dynamic chat completion routes
80
+ routes = self._get_routes()
81
+ if debug:
82
+ print(f"Registering routes: {routes}")
83
+ for path in routes:
84
+ self._register_route(path)
85
+ existing_routes = [route.path for route in self.app.routes if hasattr(route, 'path')]
86
+ if debug:
87
+ print(f"All routes now: {existing_routes}")
88
+
89
+ def _get_routes(self) -> List[str]:
90
+ """Get configured API routes"""
91
+ default_path = "/api/v1/chat/completions"
92
+ replace_chat = os.getenv("REPLACE_CHAT", "")
93
+ prefix_chat = os.getenv("PREFIX_CHAT", "")
94
+ append_chat = os.getenv("APPEND_CHAT", "")
95
+
96
+ if replace_chat:
97
+ return [path.strip() for path in replace_chat.split(",") if path.strip()]
98
+
99
+ routes = []
100
+ if prefix_chat:
101
+ routes.extend(f"{prefix.rstrip('/')}{default_path}"
102
+ for prefix in prefix_chat.split(","))
103
+ return routes
104
+
105
+ if append_chat:
106
+ append_paths = [path.strip() for path in append_chat.split(",") if path.strip()]
107
+ routes = [default_path] + append_paths
108
+ return routes
109
+
110
+ return [default_path]
111
+
112
+ def _register_route(self, path: str) -> None:
113
+ """Register a single API route"""
114
+ global debug
115
+
116
+ async def chat_endpoint(request: Request) -> Dict[str, Any]:
117
+ try:
118
+ if debug:
119
+ print(f"Request chat_endpoint...")
120
+ headers = dict(request.headers)
121
+ data = await request.json()
122
+ if debug:
123
+ print(f"Request received...\r\n\tHeaders: {headers},\r\n\tData: {data}")
124
+ return self._generate_response(headers, data)
125
+ except Exception as e:
126
+ if debug:
127
+ print(f"Request processing error: {e}")
128
+ raise HTTPException(status_code=500, detail="Internal server error") from e
129
+
130
+ self.app.post(path)(chat_endpoint)
131
+
132
+ def _calculate_tokens(self, text: str) -> int:
133
+ """Calculate token count for text"""
134
+ return len(self.encoding.encode(text))
135
+
136
+
137
+ def process_result(self, result, model):
138
+ # 如果result是字符串,尝试将其转换为JSON
139
+ if isinstance(result, str):
140
+ try:
141
+ result = json.loads(result) # 转换为JSON
142
+ except json.JSONDecodeError:
143
+ return result
144
+
145
+ # 确保result是一个字典(JSON对象)
146
+ if isinstance(result, dict):
147
+ # 设置新的id和object值
148
+ result['id'] = forts._generate_id() # 根据需要设置新的ID值
149
+ result['object'] = "chat.completion" # 根据需要设置新的object值
150
+
151
+ # 添加model值
152
+ result['model'] = model # 根据需要设置model值
153
+ return result
154
+
155
+ def _generate_response(self, headers: Dict[str, str], data: Dict[str, Any]) -> Dict[str, Any]:
156
+ """Generate API response"""
157
+ global debug
158
+ if debug:
159
+ print("inside _generate_response")
160
+ try:
161
+ # check model
162
+ model = data.get("model")
163
+ # print(f"model: {model}")
164
+ # just auto will check
165
+ if "auto" == model:
166
+ model = dg.get_auto_model()
167
+ # else:
168
+ # if not dg.is_model_available(model):
169
+ # raise HTTPException(status_code=400, detail="Invalid Model")
170
+ # ## kuan
171
+ # model = dg.get_model_by_autoupdate(model)
172
+ # must has token ? token check
173
+ authorization = headers.get('Authorization')
174
+ token = os.getenv("TOKEN", "")
175
+ if token and token not in authorization:
176
+ raise HTTPException(status_code=401, detail="无效的Token")
177
+
178
+ # call ai
179
+ msgs = data.get("messages")
180
+ if not msgs:
181
+ raise HTTPException(status_code=400, detail="消息不能为空")
182
+
183
+ if debug:
184
+ print(f"request model: {model}")
185
+ if token:
186
+ print(f"request token: {token}")
187
+ print(f"request messages: {msgs}")
188
+
189
+ result = dg.chat_completion_messages(
190
+ messages=msgs,
191
+ model=model
192
+ )
193
+ if debug:
194
+ print(f"result: {result}---- {forts.is_chatgpt_format(result)}")
195
+
196
+ # If the request body data already matches ChatGPT format, return it directly
197
+ if forts.is_chatgpt_format(result):
198
+ # If data already follows ChatGPT format, use it directly
199
+ response_data = self.process_result(result, model)
200
+ else:
201
+ # Calculate the current timestamp
202
+ current_timestamp = int(time.time() * 1000)
203
+ # Otherwise, calculate the tokens and return a structured response
204
+ prompt_tokens = self._calculate_tokens(str(data))
205
+ completion_tokens = self._calculate_tokens(result)
206
+ total_tokens = prompt_tokens + completion_tokens
207
+
208
+ response_data = {
209
+ "id": forts._generate_id(),
210
+ "object": "chat.completion",
211
+ "created": current_timestamp,
212
+ "model": data.get("model", "gpt-4o"),
213
+ "usage": {
214
+ "prompt_tokens": prompt_tokens,
215
+ "completion_tokens": completion_tokens,
216
+ "total_tokens": total_tokens
217
+ },
218
+ "choices": [{
219
+ "message": {
220
+ "role": "assistant",
221
+ "content": result
222
+ },
223
+ "finish_reason": "stop",
224
+ "index": 0
225
+ }]
226
+ }
227
+
228
+ # Print the response for debugging (you may remove this in production)
229
+ if debug:
230
+ print(f"Response Data: {response_data}")
231
+
232
+ return response_data
233
+ except Exception as e:
234
+ dg.record_call(model,False)
235
+ if debug:
236
+ print(f"Response generation error: {e}")
237
+ raise HTTPException(status_code=500, detail=str(e)) from e
238
+
239
+ def _get_workers_count(self) -> int:
240
+ """Calculate optimal worker count"""
241
+ try:
242
+ cpu_cores = multiprocessing.cpu_count()
243
+ recommended_workers = (2 * cpu_cores) + 1
244
+ return min(max(4, recommended_workers), 8)
245
+ except Exception as e:
246
+ if debug:
247
+ print(f"Worker count calculation failed: {e}, using default 4")
248
+ return 4
249
+
250
+ def get_server_config(self, host: str = "0.0.0.0", port: int = 7860) -> uvicorn.Config:
251
+ """Get server configuration"""
252
+ workers = self._get_workers_count()
253
+ if debug:
254
+ print(f"Configuring server with {workers} workers")
255
+
256
+ return uvicorn.Config(
257
+ app=self.app,
258
+ host=host,
259
+ port=port,
260
+ workers=workers,
261
+ loop="uvloop",
262
+ limit_concurrency=1000,
263
+ timeout_keep_alive=30,
264
+ access_log=True,
265
+ log_level="info",
266
+ http="httptools"
267
+ )
268
+
269
+ def run(self, host: str = "0.0.0.0", port: int = 7860) -> None:
270
+ """Run the API server"""
271
+ config = self.get_server_config(host, port)
272
+ server = uvicorn.Server(config)
273
+ server.run()
274
+
275
+ def _reload_check(self) -> None:
276
+ dg.reload_check()
277
+
278
+
279
+ def _reload_routes_if_needed(self) -> None:
280
+ """Check if routes need to be reloaded based on environment variables"""
281
+ # reload Debug
282
+ global debug
283
+ debug = os.getenv("DEBUG", "False").lower() in ["true", "1", "t"]
284
+ # relaod routes
285
+ new_routes = self._get_routes()
286
+ current_routes = [route for route in self.app.routes if hasattr(route, 'path')]
287
+
288
+ # Check if the current routes are different from the new routes
289
+ if [route.path for route in current_routes] != new_routes:
290
+ if debug:
291
+ print("Routes changed, reloading...")
292
+ self._reload_routes(new_routes)
293
+
294
+ # def _reload_routes(self, new_routes: List[str]) -> None:
295
+ # """Reload the routes based on the updated configuration"""
296
+ # # Clear existing routes
297
+ # self.app.routes.clear()
298
+ # # Register new routes
299
+ # for path in new_routes:
300
+ # self._register_route(path)
301
+
302
+ def _reload_routes(self, new_routes: List[str]) -> None:
303
+ """Reload only dynamic routes while preserving static ones"""
304
+ # Define static route names
305
+ static_routes = {"root", "web", "health", "models"}
306
+
307
+ # Remove only dynamic routes
308
+ self.app.routes[:] = [
309
+ route for route in self.app.routes
310
+ if not hasattr(route, 'name') or route.name in static_routes
311
+ ]
312
+
313
+ # Register new dynamic routes
314
+ for path in new_routes:
315
+ self._register_route(path)
316
+
317
+
318
+
319
+ def create_server() -> APIServer:
320
+ """Factory function to create server instance"""
321
+ return APIServer(app)
322
+
323
+
324
+ if __name__ == "__main__":
325
+ port = int(os.getenv("PORT", "7860"))
326
+ server = create_server()
327
+ server.run(port=port)
requirements.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ playwright
2
+ aiohttp
3
+ apscheduler
4
+ beautifulsoup4
5
+ fastapi
6
+ requests
7
+ starlette
8
+ tiktoken
9
+ urllib3
10
+ uvicorn
11
+ uvloop
12
+ httptools
13
+