Compare commits
20 Commits
v0.9.14
...
feature/ko
| Author | SHA1 | Date | |
|---|---|---|---|
| 49e0914416 | |||
| 5b74c78ec3 | |||
| c911410276 | |||
| 37ca4bf11f | |||
| 8ef084c22a | |||
| 57f0cd728f | |||
| 627a29f57e | |||
| 650c500f47 | |||
| 86acbe51e9 | |||
| 4900a7e0ad | |||
| 34da08126b | |||
| 00f416c8bc | |||
| 9c7d0a4486 | |||
| e3b9d6723f | |||
| ef80399a90 | |||
| bfbfa9d9be | |||
| 6b7be4d3b0 | |||
| 7c19c52d9f | |||
| a5f4ae9bdc | |||
| 9320815d3f |
10
README.md
10
README.md
@ -1,4 +1,4 @@
|
||||
# 此方 bot!
|
||||
# konabot
|
||||
|
||||
在 MTTU 内部使用的 bot 一只。
|
||||
|
||||
@ -63,6 +63,14 @@ code .
|
||||
|
||||
配置 `ENABLE_CONSOLE=false`
|
||||
|
||||
#### 配置并支持 LLM(大语言模型)
|
||||
|
||||
详见[LLM 配置文档](/docs/LLM.md)。
|
||||
|
||||
#### 配置 konabot-web 以支持更高级的图片渲染
|
||||
|
||||
详见[konabot-web 配置文档](/docs/konabot-web.md)
|
||||
|
||||
### 运行
|
||||
|
||||
使用命令行手动启动 Bot:
|
||||
|
||||
BIN
assets/img/meme/kiosay.jpg
Executable file
BIN
assets/img/meme/kiosay.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
65
docs/LLM.md
Normal file
65
docs/LLM.md
Normal file
@ -0,0 +1,65 @@
|
||||
# 大语言模型平台接入
|
||||
|
||||
为实现更多神秘小功能,此方 Bot 需要接入 AI。如果你需要参与开发或测试涉及 AI 的相关功能,麻烦请根据下面的文档继续操作。
|
||||
|
||||
## 配置项目接入 AI
|
||||
|
||||
AI 相关的配置文件在 `data/config/llm.json` 文件中。示例格式如下,这也将是到时候在云端的配置文件格式(给出的模型都会有):
|
||||
|
||||
```json
|
||||
{
|
||||
"llms": {
|
||||
"Qwen2.5-7B-Instruct": {
|
||||
"base_url": "https://api.siliconflow.cn/v1",
|
||||
"api_key": "sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||
"model_name": "Qwen/Qwen2.5-7B-Instruct"
|
||||
},
|
||||
"qwen3-max": {
|
||||
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
"api_key": "sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||
"model_name": "qwen3-max"
|
||||
}
|
||||
},
|
||||
"default_llm": "Qwen2.5-7B-Instruct"
|
||||
}
|
||||
```
|
||||
|
||||
其中,形如 `qwen3-max` 的名称,是你在程序中调用 LLM 使用的键名。若不给出,则会默认使用配置文件中指定的默认模型。
|
||||
|
||||
```python
|
||||
from konabot.common.llm import get_llm
|
||||
|
||||
llm = get_llm() # 获得的是 Qwen2.5-7B-Instruct 模型
|
||||
llm = get_llm("qwen3-max") # 获得的是 qwen3-max 模型
|
||||
|
||||
message = await llm.chat([
|
||||
{ "role": "system", "content": "你是一只猫娘" },
|
||||
{ "role": "user", "content": "晚上好呀!" },
|
||||
], timeout=None, max_tokens=16384)
|
||||
# 获得了的是 openai.types.chat.ChatCompletionMessage 对象
|
||||
|
||||
print(f"AI 返回值:{message.content}") # 注意 content 可能为 None,需要做空值检测
|
||||
|
||||
client = llm.get_openai_client() # 获得的是一个 OpenAI Client 对象,可以做更多操作
|
||||
# 例如,调用 Embedding 模型来做知识库向量化等工作
|
||||
```
|
||||
|
||||
## 本项目使用的模型清单
|
||||
|
||||
为了便利大家使用,我在这里给出该项目将会使用的模型清单,请根据你的开发需求注册并选择你最喜欢的模型。如果需要接入新的模型,或者使用到文档之外的模型,欢迎在这里给出!
|
||||
|
||||
### 硅基流动 Qwen/Qwen2.5-7B-Instruct
|
||||
|
||||
一个 7B 大小的 AI 模型。其性能不太能指望,但是它小,而且比较快,可以做一些轻量的操作。
|
||||
|
||||
该模型是免费的,但是也需要你注册[硅基流动](https://cloud.siliconflow.cn/me/models)账号,并生成 `api_key` 添加到配置文件中。
|
||||
|
||||
### 通义千问 qwen3-max
|
||||
|
||||
贵但是很先进的最新模型,其能力可以信赖。但是不要拿它做大量工作哦!
|
||||
|
||||
在[百炼大模型平台](https://bailian.console.aliyun.com/)注册账号并申请 `api_key`,新用户会赠送 1M tokens,足够做测试了。
|
||||
|
||||
## 安全须知
|
||||
|
||||
请注意提防 AI 越狱等情况。
|
||||
18
docs/konabot-web.md
Normal file
18
docs/konabot-web.md
Normal file
@ -0,0 +1,18 @@
|
||||
# konabot-web 配置文档
|
||||
|
||||
本文档教你配置一个此方 Bot 的 Web 服务器。
|
||||
|
||||
## 安装并运行 konabot-web
|
||||
|
||||
按照 [konabot-web README](https://gitea.service.jazzwhom.top/mttu-developers/konabot-web) 安装并运行 konabot-web 实例。
|
||||
|
||||
## 指定 konabot-web 实例地址
|
||||
|
||||
如果你的 Web 服务器的端口不是 5173,或者你有特殊的网络结构,你需要手动设置 konabot-web。编辑 `.env` 文件:
|
||||
|
||||
```
|
||||
MODULE_WEB_RENDER_WEBURL=http://web-server:port
|
||||
MODULE_WEB_RENDER_INSTANCE=http://konabot-server:port
|
||||
```
|
||||
|
||||
替换 web-server 为你的前端服务器地址,konabot-server 为后端服务器地址,port 为端口号。
|
||||
64
konabot/common/llm/__init__.py
Normal file
64
konabot/common/llm/__init__.py
Normal file
@ -0,0 +1,64 @@
|
||||
from typing import Any
|
||||
import openai
|
||||
|
||||
from loguru import logger
|
||||
from openai.types.chat import ChatCompletion, ChatCompletionMessage, ChatCompletionMessageParam
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from konabot.common.path import CONFIG_PATH
|
||||
|
||||
LLM_CONFIG_PATH = CONFIG_PATH / 'llm.json'
|
||||
|
||||
if not LLM_CONFIG_PATH.exists():
|
||||
LLM_CONFIG_PATH.write_text("{}")
|
||||
|
||||
|
||||
class LLMInfo(BaseModel):
|
||||
base_url: str
|
||||
api_key: str
|
||||
model_name: str
|
||||
|
||||
def get_openai_client(self):
|
||||
return openai.AsyncClient(
|
||||
api_key=self.api_key,
|
||||
base_url=self.base_url,
|
||||
)
|
||||
|
||||
async def chat(
|
||||
self,
|
||||
messages: list[ChatCompletionMessageParam],
|
||||
timeout: float | None = 30.0,
|
||||
max_tokens: int | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ChatCompletionMessage:
|
||||
logger.info(f"调用 LLM: BASE_URL={self.base_url} MODEL_NAME={self.model_name}")
|
||||
completion: ChatCompletion = await self.get_openai_client().chat.completions.create(
|
||||
messages=messages,
|
||||
model=self.model_name,
|
||||
max_tokens=max_tokens,
|
||||
timeout=timeout,
|
||||
stream=False,
|
||||
**kwargs,
|
||||
)
|
||||
choice = completion.choices[0]
|
||||
logger.info(
|
||||
f"调用 LLM 完成: BASE_URL={self.base_url} MODEL_NAME={self.model_name} REASON={choice.finish_reason}"
|
||||
)
|
||||
return choice.message
|
||||
|
||||
|
||||
class LLMConfig(BaseModel):
|
||||
llms: dict[str, LLMInfo] = Field(default_factory=dict)
|
||||
default_llm: str = "Qwen2.5-7B-Instruct"
|
||||
|
||||
|
||||
llm_config = LLMConfig.model_validate_json(LLM_CONFIG_PATH.read_text())
|
||||
|
||||
|
||||
def get_llm(llm_model: str | None = None):
|
||||
if llm_model is None:
|
||||
llm_model = llm_config.default_llm
|
||||
if llm_model not in llm_config.llms:
|
||||
raise NotImplementedError("LLM 未配置,该功能无法使用")
|
||||
return llm_config.llms[llm_model]
|
||||
|
||||
@ -8,20 +8,38 @@ import nonebot
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.adapters import Bot, Event, Message
|
||||
from nonebot.adapters.discord import Bot as DiscordBot
|
||||
from nonebot.adapters.discord import MessageEvent as DiscordMessageEvent
|
||||
from nonebot.adapters.discord.config import Config as DiscordConfig
|
||||
from nonebot.adapters.onebot.v11 import Bot as OnebotV11Bot
|
||||
from nonebot.adapters.onebot.v11 import Message as OnebotV11Message
|
||||
from nonebot.adapters.onebot.v11 import MessageEvent as OnebotV11MessageEvent
|
||||
import nonebot.params
|
||||
from nonebot_plugin_alconna import Image, RefNode, Reply, UniMessage
|
||||
from PIL import UnidentifiedImageError
|
||||
from pydantic import BaseModel
|
||||
from returns.result import Failure, Result, Success
|
||||
|
||||
from konabot.common.path import ASSETS_PATH
|
||||
|
||||
async def download_image_bytes(url: str) -> Result[bytes, str]:
|
||||
|
||||
discordConfig = nonebot.get_plugin_config(DiscordConfig)
|
||||
|
||||
|
||||
class ExtractImageConfig(BaseModel):
|
||||
module_extract_image_no_download: bool = False
|
||||
"要不要算了,不下载了,直接爆炸算了,适用于一些比较奇怪的网络环境,无法从协议端下载文件"
|
||||
|
||||
|
||||
module_config = nonebot.get_plugin_config(ExtractImageConfig)
|
||||
|
||||
|
||||
async def download_image_bytes(url: str, proxy: str | None = None) -> Result[bytes, str]:
|
||||
# if "/matcha/cache/" in url:
|
||||
# url = url.replace('127.0.0.1', '10.126.126.101')
|
||||
if module_config.module_extract_image_no_download:
|
||||
return Success((ASSETS_PATH / "img" / "other" / "boom.jpg").read_bytes())
|
||||
logger.debug(f"开始从 {url} 下载图片")
|
||||
async with httpx.AsyncClient() as c:
|
||||
async with httpx.AsyncClient(proxy=proxy) as c:
|
||||
try:
|
||||
response = await c.get(url)
|
||||
except (httpx.ConnectError, httpx.RemoteProtocolError) as e:
|
||||
@ -121,6 +139,14 @@ async def extract_image_from_message(
|
||||
logger.debug('获取图片的路径 Fallback 到 QQ 模块')
|
||||
return await extract_image_from_qq_message(msg, evt, bot, allow_reply)
|
||||
|
||||
if isinstance(evt, DiscordMessageEvent):
|
||||
logger.debug('获取图片的路径方式走 Discord')
|
||||
for a in evt.attachments:
|
||||
if "image/" not in a.content_type:
|
||||
continue
|
||||
url = a.proxy_url
|
||||
return (await download_image_bytes(url, discordConfig.discord_proxy)).bind(bytes_to_pil)
|
||||
|
||||
for seg in UniMessage.of(msg, bot):
|
||||
logger.info(seg)
|
||||
if isinstance(seg, Image):
|
||||
|
||||
@ -6,6 +6,7 @@ FONTS_PATH = ASSETS_PATH / "fonts"
|
||||
SRC_PATH = Path(__file__).resolve().parent.parent
|
||||
DATA_PATH = SRC_PATH.parent / "data"
|
||||
LOG_PATH = DATA_PATH / "logs"
|
||||
CONFIG_PATH = DATA_PATH / "config"
|
||||
|
||||
DOCS_PATH = SRC_PATH / "docs"
|
||||
DOCS_PATH_MAN1 = DOCS_PATH / "user"
|
||||
@ -19,3 +20,5 @@ if not DATA_PATH.exists():
|
||||
if not LOG_PATH.exists():
|
||||
LOG_PATH.mkdir()
|
||||
|
||||
CONFIG_PATH.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
17
konabot/common/utils/to_async.py
Normal file
17
konabot/common/utils/to_async.py
Normal file
@ -0,0 +1,17 @@
|
||||
import asyncio
|
||||
import functools
|
||||
|
||||
from typing import Awaitable, Callable, ParamSpec, TypeVar
|
||||
|
||||
|
||||
TA = ParamSpec("TA")
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def make_async(func: Callable[TA, T]) -> Callable[TA, Awaitable[T]]:
|
||||
@functools.wraps(func, assigned=("__module__", "__name__", "__qualname__", "__doc__", "__annotations__"))
|
||||
async def wrapper(*args: TA.args, **kwargs: TA.kwargs):
|
||||
return await asyncio.to_thread(func, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
@ -1,211 +1,9 @@
|
||||
import asyncio
|
||||
import queue
|
||||
from typing import Any, Callable, Coroutine
|
||||
from loguru import logger
|
||||
from playwright.async_api import Page, Playwright, async_playwright, Browser, Page, BrowserContext
|
||||
from .config import web_render_config
|
||||
from .core import WebRenderer as WebRenderer
|
||||
from .core import WebRendererInstance as WebRendererInstance
|
||||
|
||||
|
||||
PageFunction = Callable[[Page], Coroutine[Any, Any, Any]]
|
||||
|
||||
|
||||
class WebRenderer:
|
||||
browser_pool: queue.Queue["WebRendererInstance"] = queue.Queue()
|
||||
context_pool: dict[int, BrowserContext] = {} # 长期挂载的浏览器上下文池
|
||||
page_pool: dict[str, Page] = {} # 长期挂载的页面池
|
||||
|
||||
@classmethod
|
||||
async def get_browser_instance(cls) -> "WebRendererInstance":
|
||||
if cls.browser_pool.empty():
|
||||
instance = await WebRendererInstance.create()
|
||||
cls.browser_pool.put(instance)
|
||||
instance = cls.browser_pool.get()
|
||||
cls.browser_pool.put(instance)
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
async def get_browser_context(cls) -> BrowserContext:
|
||||
instance = await cls.get_browser_instance()
|
||||
if id(instance) not in cls.context_pool:
|
||||
context = await instance.browser.new_context()
|
||||
cls.context_pool[id(instance)] = context
|
||||
logger.debug(f"Created new persistent browser context for WebRendererInstance {id(instance)}")
|
||||
return cls.context_pool[id(instance)]
|
||||
|
||||
@classmethod
|
||||
async def render(
|
||||
cls,
|
||||
url: str,
|
||||
target: str,
|
||||
params: dict = {},
|
||||
other_function: PageFunction | None = None,
|
||||
timeout: int = 30,
|
||||
) -> bytes:
|
||||
'''
|
||||
访问指定URL并返回截图
|
||||
|
||||
:param url: 目标URL
|
||||
:param target: 渲染目标,如 ".box"、"#main" 等CSS选择器
|
||||
:param timeout: 页面加载超时时间,单位秒
|
||||
:param params: URL键值对参数
|
||||
:param other_function: 其他自定义操作函数,接受page参数
|
||||
:return: 截图的字节数据
|
||||
|
||||
'''
|
||||
instance = await cls.get_browser_instance()
|
||||
logger.debug(f"Using WebRendererInstance {id(instance)} to render {url} targeting {target}")
|
||||
return await instance.render(url, target, params=params, other_function=other_function, timeout=timeout)
|
||||
|
||||
|
||||
@classmethod
|
||||
async def render_persistent_page(cls, page_id: str, url: str, target: str, params: dict = {}, other_function: callable = None, timeout: int = 30) -> bytes:
|
||||
'''
|
||||
使用长期挂载的页面访问指定URL并返回截图
|
||||
|
||||
:param page_id: 页面唯一标识符
|
||||
:param url: 目标URL
|
||||
:param target: 渲染目标,如 ".box"、"#main" 等CSS选择器
|
||||
:param timeout: 页面加载超时时间,单位秒
|
||||
:param params: URL键值对参数
|
||||
:param other_function: 其他自定义操作函数,接受page参数
|
||||
:return: 截图的字节数据
|
||||
|
||||
'''
|
||||
logger.debug(f"Requesting persistent render for page_id {page_id} at {url} targeting {target} with timeout {timeout}")
|
||||
instance = await cls.get_browser_instance()
|
||||
if page_id not in cls.page_pool:
|
||||
context = await cls.get_browser_context()
|
||||
page = await context.new_page()
|
||||
cls.page_pool[page_id] = page
|
||||
logger.debug(f"Created new persistent page for page_id {page_id} using WebRendererInstance {id(instance)}")
|
||||
page = cls.page_pool[page_id]
|
||||
return await instance.render_with_page(page, url, target, params=params, other_function=other_function, timeout=timeout)
|
||||
|
||||
@classmethod
|
||||
async def render_file(
|
||||
cls,
|
||||
file_path: str,
|
||||
target: str,
|
||||
params: dict = {},
|
||||
other_function: PageFunction | None = None,
|
||||
timeout: int = 30,
|
||||
) -> bytes:
|
||||
'''
|
||||
访问指定本地文件URL并返回截图
|
||||
|
||||
:param file_path: 目标文件路径
|
||||
:param target: 渲染目标,如 ".box"、"#main" 等CSS选择器
|
||||
:param timeout: 页面加载超时时间,单位秒
|
||||
:param params: URL键值对参数
|
||||
:param other_function: 其他自定义操作函数,接受page参数
|
||||
:return: 截图的字节数据
|
||||
|
||||
'''
|
||||
instance = await cls.get_browser_instance()
|
||||
logger.debug(f"Using WebRendererInstance {id(instance)} to render file {file_path} targeting {target}")
|
||||
return await instance.render_file(file_path, target, params=params, other_function=other_function, timeout=timeout)
|
||||
|
||||
@classmethod
|
||||
async def close_persistent_page(cls, page_id: str) -> None:
|
||||
'''
|
||||
关闭并移除长期挂载的页面
|
||||
|
||||
:param page_id: 页面唯一标识符
|
||||
'''
|
||||
if page_id in cls.page_pool:
|
||||
page = cls.page_pool[page_id]
|
||||
await page.close()
|
||||
del cls.page_pool[page_id]
|
||||
logger.debug(f"Closed and removed persistent page for page_id {page_id}")
|
||||
|
||||
|
||||
|
||||
class WebRendererInstance:
|
||||
def __init__(self):
|
||||
self._playwright: Playwright | None = None
|
||||
self._browser: Browser | None = None
|
||||
self.lock = asyncio.Lock()
|
||||
|
||||
@property
|
||||
def playwright(self) -> Playwright:
|
||||
assert self._playwright is not None
|
||||
return self._playwright
|
||||
|
||||
@property
|
||||
def browser(self) -> Browser:
|
||||
assert self._browser is not None
|
||||
return self._browser
|
||||
|
||||
async def init(self):
|
||||
self._playwright = await async_playwright().start()
|
||||
self._browser = await self.playwright.chromium.launch(headless=True)
|
||||
|
||||
@classmethod
|
||||
async def create(cls) -> "WebRendererInstance":
|
||||
instance = cls()
|
||||
await instance.init()
|
||||
return instance
|
||||
|
||||
async def render(
|
||||
self,
|
||||
url: str,
|
||||
target: str,
|
||||
index: int = 0,
|
||||
params: dict = {},
|
||||
other_function: PageFunction | None = None,
|
||||
timeout: int = 30
|
||||
) -> bytes:
|
||||
'''
|
||||
访问指定URL并返回截图
|
||||
|
||||
:param url: 目标URL
|
||||
:param target: 渲染目标,如 ".box"、"#main" 等CSS选择器
|
||||
:param timeout: 页面加载超时时间,单位秒
|
||||
:param index: 如果目标是一个列表,指定要截图的元素索引
|
||||
:param params: URL键值对参数
|
||||
:param other_function: 其他自定义操作函数,接受page参数
|
||||
:return: 截图的字节数据
|
||||
|
||||
'''
|
||||
async with self.lock:
|
||||
context = await self.browser.new_context()
|
||||
page = await context.new_page()
|
||||
screenshot = await self.inner_render(page, url, target, index, params, other_function, timeout)
|
||||
await page.close()
|
||||
await context.close()
|
||||
return screenshot
|
||||
|
||||
async def render_with_page(self, page: Page, url: str, target: str, index: int = 0, params: dict = {}, other_function: callable = None, timeout: int = 30) -> bytes:
|
||||
async with self.lock:
|
||||
screenshot = await self.inner_render(page, url, target, index, params, other_function, timeout)
|
||||
return screenshot
|
||||
|
||||
async def render_file(self, file_path: str, target: str, index: int = 0, params: dict = {}, other_function: callable = None, timeout: int = 30) -> bytes:
|
||||
file_path = "file:///" + str(file_path).replace("\\", "/")
|
||||
return await self.render(file_path, target, index, params, other_function, timeout)
|
||||
|
||||
async def inner_render(self, page: Page, url: str, target: str, index: int = 0, params: dict = {}, other_function: callable = None, timeout: int = 30) -> bytes:
|
||||
logger.debug(f"Navigating to {url} with timeout {timeout}")
|
||||
url_with_params = url + ("?" + "&".join(f"{k}={v}" for k, v in params.items()) if params else "")
|
||||
await page.goto(url_with_params, timeout=timeout * 1000, wait_until="load")
|
||||
logger.debug("Page loaded successfully")
|
||||
# 等待目标元素出现
|
||||
await page.wait_for_selector(target, timeout=timeout * 1000)
|
||||
logger.debug(f"Target element '{target}' found, taking screenshot")
|
||||
if other_function:
|
||||
await other_function(page)
|
||||
elements = await page.query_selector_all(target)
|
||||
if not elements:
|
||||
logger.error(f"Target element '{target}' not found on the page.")
|
||||
return None
|
||||
if index >= len(elements):
|
||||
logger.error(f"Index {index} out of range for elements matching '{target}'")
|
||||
return None
|
||||
element = elements[index]
|
||||
screenshot = await element.screenshot()
|
||||
logger.debug(f"Screenshot taken successfully")
|
||||
return screenshot
|
||||
|
||||
async def close(self):
|
||||
await self.browser.close()
|
||||
await self.playwright.stop()
|
||||
def konaweb(sub_url: str):
|
||||
sub_url = '/' + sub_url.removeprefix('/')
|
||||
return web_render_config.module_web_render_weburl.removesuffix('/') + sub_url
|
||||
|
||||
|
||||
19
konabot/common/web_render/config.py
Normal file
19
konabot/common/web_render/config.py
Normal file
@ -0,0 +1,19 @@
|
||||
import nonebot
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
class Config(BaseModel):
|
||||
module_web_render_weburl: str = "localhost:5173"
|
||||
module_web_render_instance: str = ""
|
||||
|
||||
def get_instance_baseurl(self):
|
||||
if self.module_web_render_instance:
|
||||
return self.module_web_render_instance.removesuffix('/')
|
||||
config = nonebot.get_driver().config
|
||||
ip = str(config.host)
|
||||
if ip == "0.0.0.0":
|
||||
ip = "127.0.0.1"
|
||||
port = config.port
|
||||
return f'http://{ip}:{port}'
|
||||
|
||||
web_render_config = nonebot.get_plugin_config(Config)
|
||||
281
konabot/common/web_render/core.py
Normal file
281
konabot/common/web_render/core.py
Normal file
@ -0,0 +1,281 @@
|
||||
import asyncio
|
||||
import queue
|
||||
from typing import Any, Callable, Coroutine
|
||||
from loguru import logger
|
||||
from playwright.async_api import (
|
||||
Page,
|
||||
Playwright,
|
||||
async_playwright,
|
||||
Browser,
|
||||
BrowserContext,
|
||||
)
|
||||
|
||||
|
||||
PageFunction = Callable[[Page], Coroutine[Any, Any, Any]]
|
||||
|
||||
|
||||
class WebRenderer:
|
||||
browser_pool: queue.Queue["WebRendererInstance"] = queue.Queue()
|
||||
context_pool: dict[int, BrowserContext] = {} # 长期挂载的浏览器上下文池
|
||||
page_pool: dict[str, Page] = {} # 长期挂载的页面池
|
||||
|
||||
@classmethod
|
||||
async def get_browser_instance(cls) -> "WebRendererInstance":
|
||||
if cls.browser_pool.empty():
|
||||
instance = await WebRendererInstance.create()
|
||||
cls.browser_pool.put(instance)
|
||||
instance = cls.browser_pool.get()
|
||||
cls.browser_pool.put(instance)
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
async def get_browser_context(cls) -> BrowserContext:
|
||||
instance = await cls.get_browser_instance()
|
||||
if id(instance) not in cls.context_pool:
|
||||
context = await instance.browser.new_context()
|
||||
cls.context_pool[id(instance)] = context
|
||||
logger.debug(
|
||||
f"Created new persistent browser context for WebRendererInstance {id(instance)}"
|
||||
)
|
||||
return cls.context_pool[id(instance)]
|
||||
|
||||
@classmethod
|
||||
async def render(
|
||||
cls,
|
||||
url: str,
|
||||
target: str,
|
||||
params: dict = {},
|
||||
other_function: PageFunction | None = None,
|
||||
timeout: int = 30,
|
||||
) -> bytes:
|
||||
"""
|
||||
访问指定URL并返回截图
|
||||
|
||||
:param url: 目标URL
|
||||
:param target: 渲染目标,如 ".box"、"#main" 等CSS选择器
|
||||
:param timeout: 页面加载超时时间,单位秒
|
||||
:param params: URL键值对参数
|
||||
:param other_function: 其他自定义操作函数,接受page参数
|
||||
:return: 截图的字节数据
|
||||
|
||||
"""
|
||||
instance = await cls.get_browser_instance()
|
||||
logger.debug(
|
||||
f"Using WebRendererInstance {id(instance)} to render {url} targeting {target}"
|
||||
)
|
||||
return await instance.render(
|
||||
url, target, params=params, other_function=other_function, timeout=timeout
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def render_persistent_page(
|
||||
cls,
|
||||
page_id: str,
|
||||
url: str,
|
||||
target: str,
|
||||
params: dict = {},
|
||||
other_function: PageFunction | None = None,
|
||||
timeout: int = 30,
|
||||
) -> bytes:
|
||||
"""
|
||||
使用长期挂载的页面访问指定URL并返回截图
|
||||
|
||||
:param page_id: 页面唯一标识符
|
||||
:param url: 目标URL
|
||||
:param target: 渲染目标,如 ".box"、"#main" 等CSS选择器
|
||||
:param timeout: 页面加载超时时间,单位秒
|
||||
:param params: URL键值对参数
|
||||
:param other_function: 其他自定义操作函数,接受page参数
|
||||
:return: 截图的字节数据
|
||||
|
||||
"""
|
||||
logger.debug(
|
||||
f"Requesting persistent render for page_id {page_id} at {url} targeting {target} with timeout {timeout}"
|
||||
)
|
||||
instance = await cls.get_browser_instance()
|
||||
if page_id not in cls.page_pool:
|
||||
context = await cls.get_browser_context()
|
||||
page = await context.new_page()
|
||||
cls.page_pool[page_id] = page
|
||||
logger.debug(
|
||||
f"Created new persistent page for page_id {page_id} using WebRendererInstance {id(instance)}"
|
||||
)
|
||||
page = cls.page_pool[page_id]
|
||||
return await instance.render_with_page(
|
||||
page,
|
||||
url,
|
||||
target,
|
||||
params=params,
|
||||
other_function=other_function,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def render_file(
|
||||
cls,
|
||||
file_path: str,
|
||||
target: str,
|
||||
params: dict = {},
|
||||
other_function: PageFunction | None = None,
|
||||
timeout: int = 30,
|
||||
) -> bytes:
|
||||
"""
|
||||
访问指定本地文件URL并返回截图
|
||||
|
||||
:param file_path: 目标文件路径
|
||||
:param target: 渲染目标,如 ".box"、"#main" 等CSS选择器
|
||||
:param timeout: 页面加载超时时间,单位秒
|
||||
:param params: URL键值对参数
|
||||
:param other_function: 其他自定义操作函数,接受page参数
|
||||
:return: 截图的字节数据
|
||||
|
||||
"""
|
||||
instance = await cls.get_browser_instance()
|
||||
logger.debug(
|
||||
f"Using WebRendererInstance {id(instance)} to render file {file_path} targeting {target}"
|
||||
)
|
||||
return await instance.render_file(
|
||||
file_path,
|
||||
target,
|
||||
params=params,
|
||||
other_function=other_function,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def close_persistent_page(cls, page_id: str) -> None:
|
||||
"""
|
||||
关闭并移除长期挂载的页面
|
||||
|
||||
:param page_id: 页面唯一标识符
|
||||
"""
|
||||
if page_id in cls.page_pool:
|
||||
page = cls.page_pool[page_id]
|
||||
await page.close()
|
||||
del cls.page_pool[page_id]
|
||||
logger.debug(f"Closed and removed persistent page for page_id {page_id}")
|
||||
|
||||
|
||||
class WebRendererInstance:
|
||||
def __init__(self):
|
||||
self._playwright: Playwright | None = None
|
||||
self._browser: Browser | None = None
|
||||
self.lock = asyncio.Lock()
|
||||
|
||||
@property
|
||||
def playwright(self) -> Playwright:
|
||||
assert self._playwright is not None
|
||||
return self._playwright
|
||||
|
||||
@property
|
||||
def browser(self) -> Browser:
|
||||
assert self._browser is not None
|
||||
return self._browser
|
||||
|
||||
async def init(self):
|
||||
self._playwright = await async_playwright().start()
|
||||
self._browser = await self.playwright.chromium.launch(headless=True)
|
||||
|
||||
@classmethod
|
||||
async def create(cls) -> "WebRendererInstance":
|
||||
instance = cls()
|
||||
await instance.init()
|
||||
return instance
|
||||
|
||||
async def render(
|
||||
self,
|
||||
url: str,
|
||||
target: str,
|
||||
index: int = 0,
|
||||
params: dict = {},
|
||||
other_function: PageFunction | None = None,
|
||||
timeout: int = 30,
|
||||
) -> bytes:
|
||||
"""
|
||||
访问指定URL并返回截图
|
||||
|
||||
:param url: 目标URL
|
||||
:param target: 渲染目标,如 ".box"、"#main" 等CSS选择器
|
||||
:param timeout: 页面加载超时时间,单位秒
|
||||
:param index: 如果目标是一个列表,指定要截图的元素索引
|
||||
:param params: URL键值对参数
|
||||
:param other_function: 其他自定义操作函数,接受page参数
|
||||
:return: 截图的字节数据
|
||||
|
||||
"""
|
||||
async with self.lock:
|
||||
context = await self.browser.new_context()
|
||||
page = await context.new_page()
|
||||
screenshot = await self.inner_render(
|
||||
page, url, target, index, params, other_function, timeout
|
||||
)
|
||||
await page.close()
|
||||
await context.close()
|
||||
return screenshot
|
||||
|
||||
async def render_with_page(
|
||||
self,
|
||||
page: Page,
|
||||
url: str,
|
||||
target: str,
|
||||
index: int = 0,
|
||||
params: dict = {},
|
||||
other_function: PageFunction | None = None,
|
||||
timeout: int = 30,
|
||||
) -> bytes:
|
||||
async with self.lock:
|
||||
screenshot = await self.inner_render(
|
||||
page, url, target, index, params, other_function, timeout
|
||||
)
|
||||
return screenshot
|
||||
|
||||
async def render_file(
|
||||
self,
|
||||
file_path: str,
|
||||
target: str,
|
||||
index: int = 0,
|
||||
params: dict = {},
|
||||
other_function: PageFunction | None = None,
|
||||
timeout: int = 30,
|
||||
) -> bytes:
|
||||
file_path = "file:///" + str(file_path).replace("\\", "/")
|
||||
return await self.render(
|
||||
file_path, target, index, params, other_function, timeout
|
||||
)
|
||||
|
||||
async def inner_render(
|
||||
self,
|
||||
page: Page,
|
||||
url: str,
|
||||
target: str,
|
||||
index: int = 0,
|
||||
params: dict = {},
|
||||
other_function: PageFunction | None = None,
|
||||
timeout: int = 30,
|
||||
) -> bytes:
|
||||
logger.debug(f"Navigating to {url} with timeout {timeout}")
|
||||
url_with_params = url + (
|
||||
"?" + "&".join(f"{k}={v}" for k, v in params.items()) if params else ""
|
||||
)
|
||||
await page.goto(url_with_params, timeout=timeout * 1000, wait_until="load")
|
||||
logger.debug("Page loaded successfully")
|
||||
# 等待目标元素出现
|
||||
await page.wait_for_selector(target, timeout=timeout * 1000)
|
||||
logger.debug(f"Target element '{target}' found, taking screenshot")
|
||||
if other_function:
|
||||
await other_function(page)
|
||||
elements = await page.query_selector_all(target)
|
||||
if not elements:
|
||||
logger.warning(f"Target element '{target}' not found on the page.")
|
||||
elements = await page.query_selector_all('body')
|
||||
if index >= len(elements):
|
||||
logger.warning(f"Index {index} out of range for elements matching '{target}'")
|
||||
index = 0
|
||||
element = elements[index]
|
||||
screenshot = await element.screenshot()
|
||||
logger.debug("Screenshot taken successfully")
|
||||
return screenshot
|
||||
|
||||
async def close(self):
|
||||
await self.browser.close()
|
||||
await self.playwright.stop()
|
||||
66
konabot/common/web_render/host_images.py
Normal file
66
konabot/common/web_render/host_images.py
Normal file
@ -0,0 +1,66 @@
|
||||
import asyncio
|
||||
import tempfile
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
from fastapi import HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
import nanoid
|
||||
import nonebot
|
||||
|
||||
from nonebot.drivers.fastapi import Driver as FastAPIDriver
|
||||
|
||||
from .config import web_render_config
|
||||
|
||||
app = cast(FastAPIDriver, nonebot.get_driver()).asgi
|
||||
|
||||
hosted_tempdirs: dict[str, Path] = {}
|
||||
hosted_tempdirs_lock = asyncio.Lock()
|
||||
|
||||
|
||||
@dataclass
|
||||
class TempDir:
|
||||
path: Path
|
||||
url_base: str
|
||||
|
||||
def url_of(self, file: Path):
|
||||
assert file.is_relative_to(self.path)
|
||||
relative_path = file.relative_to(self.path)
|
||||
url_path_segment = str(relative_path).replace("\\", "/")
|
||||
return f"{self.url_base}/{url_path_segment}"
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def host_tempdir():
|
||||
with tempfile.TemporaryDirectory() as tempdir:
|
||||
fp = Path(tempdir)
|
||||
nid = nanoid.generate(size=10)
|
||||
async with hosted_tempdirs_lock:
|
||||
hosted_tempdirs[nid] = fp
|
||||
yield TempDir(
|
||||
path=fp,
|
||||
url_base=f"{web_render_config.get_instance_baseurl()}/tempdir/{nid}",
|
||||
)
|
||||
async with hosted_tempdirs_lock:
|
||||
del hosted_tempdirs[nid]
|
||||
|
||||
|
||||
@app.get("/tempdir/{nid}/{file_path:path}")
|
||||
async def _(nid: str, file_path: str):
|
||||
async with hosted_tempdirs_lock:
|
||||
base_path = hosted_tempdirs.get(nid)
|
||||
if base_path is None:
|
||||
raise HTTPException(404)
|
||||
full_path = base_path / file_path
|
||||
try:
|
||||
if not full_path.resolve().is_relative_to(base_path.resolve()):
|
||||
raise HTTPException(status_code=403, detail="Access denied.")
|
||||
except Exception:
|
||||
raise HTTPException(status_code=403, detail="Access denied.")
|
||||
if not full_path.is_file():
|
||||
raise HTTPException(status_code=404, detail="File not found.")
|
||||
|
||||
return FileResponse(full_path.resolve())
|
||||
|
||||
@ -320,7 +320,7 @@ def create_admin_commands():
|
||||
if image is not None and image.url is not None:
|
||||
b = await download_image_bytes(image.url)
|
||||
image_manager.remove_puzzle_image(p.img_name)
|
||||
image_manager.upload_puzzle_image(b.unwrap())
|
||||
p.img_name = image_manager.upload_puzzle_image(b.unwrap())
|
||||
elif remove_image.available:
|
||||
image_manager.remove_puzzle_image(p.img_name)
|
||||
|
||||
|
||||
40
konabot/plugins/llm_test.py
Normal file
40
konabot/plugins/llm_test.py
Normal file
@ -0,0 +1,40 @@
|
||||
"""
|
||||
肥肠危险注意:本文件仅用于开发环境测试 LLM 模块能否正常工作!
|
||||
|
||||
请不要在生产环境启用它!
|
||||
"""
|
||||
|
||||
import nonebot
|
||||
from nonebot_plugin_alconna import Alconna, Args, on_alconna
|
||||
from pydantic import BaseModel
|
||||
|
||||
from konabot.common.llm import get_llm
|
||||
from konabot.common.longtask import DepLongTaskTarget
|
||||
|
||||
|
||||
class LLMTestConfig(BaseModel):
|
||||
debug_enable_llm_test: bool = False
|
||||
|
||||
|
||||
config = nonebot.get_plugin_config(LLMTestConfig)
|
||||
|
||||
|
||||
if config.debug_enable_llm_test:
|
||||
cmd = on_alconna(Alconna(
|
||||
"debug-ask-llm",
|
||||
Args["prompt", str],
|
||||
))
|
||||
|
||||
@cmd.handle()
|
||||
async def _(prompt: str, target: DepLongTaskTarget):
|
||||
llm = get_llm()
|
||||
msg = await llm.chat(
|
||||
[
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
timeout=None,
|
||||
max_tokens=1024,
|
||||
)
|
||||
content = msg.content or ""
|
||||
await target.send_message(content)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from io import BytesIO
|
||||
from typing import Iterable, cast
|
||||
|
||||
from loguru import logger
|
||||
from nonebot import on_message
|
||||
from nonebot_plugin_alconna import (
|
||||
Alconna,
|
||||
@ -14,8 +15,12 @@ from nonebot_plugin_alconna import (
|
||||
UniMsg,
|
||||
on_alconna,
|
||||
)
|
||||
from playwright.async_api import ConsoleMessage, Page
|
||||
|
||||
from konabot.common.nb.extract_image import PIL_Image, extract_image_from_message
|
||||
from konabot.common.web_render import konaweb
|
||||
from konabot.common.web_render.core import WebRenderer
|
||||
from konabot.common.web_render.host_images import host_tempdir
|
||||
from konabot.plugins.memepack.drawing.display import (
|
||||
draw_cao_display,
|
||||
draw_snaur_display,
|
||||
@ -24,6 +29,7 @@ from konabot.plugins.memepack.drawing.display import (
|
||||
from konabot.plugins.memepack.drawing.saying import (
|
||||
draw_cute_ten,
|
||||
draw_geimao,
|
||||
draw_kiosay,
|
||||
draw_mnk,
|
||||
draw_pt,
|
||||
draw_suan,
|
||||
@ -275,3 +281,64 @@ async def _(msg: UniMsg, evt: Event, bot: Bot):
|
||||
.export()
|
||||
)
|
||||
|
||||
|
||||
kiosay = on_alconna(
|
||||
Alconna(
|
||||
"西多说",
|
||||
Args[
|
||||
"saying",
|
||||
MultiVar(str, "+"),
|
||||
Field(missing_tips=lambda: "你没有写西多说了什么"),
|
||||
],
|
||||
),
|
||||
use_cmd_start=True,
|
||||
use_cmd_sep=False,
|
||||
skip_for_unmatch=False,
|
||||
aliases=set(),
|
||||
)
|
||||
|
||||
|
||||
@kiosay.handle()
|
||||
async def _(saying: list[str]):
|
||||
img = await draw_kiosay("\n".join(saying))
|
||||
img_bytes = BytesIO()
|
||||
img.save(img_bytes, format="PNG")
|
||||
|
||||
await kiosay.send(await UniMessage().image(raw=img_bytes).export())
|
||||
|
||||
|
||||
quote_cmd = on_alconna(Alconna(
|
||||
"名人名言",
|
||||
Args["quote", str],
|
||||
Args["author", str],
|
||||
Args["image?", Image | None],
|
||||
), aliases={"quote"})
|
||||
|
||||
@quote_cmd.handle()
|
||||
async def _(quote: str, author: str, img: PIL_Image):
|
||||
async with host_tempdir() as tempdir:
|
||||
img_path = tempdir.path / "image.png"
|
||||
img_url = tempdir.url_of(img_path)
|
||||
img.save(img_path)
|
||||
|
||||
async def page_function(page: Page):
|
||||
async def on_console(msg: ConsoleMessage):
|
||||
logger.debug(f"WEB CONSOLE {msg.text}")
|
||||
|
||||
page.on('console', on_console)
|
||||
|
||||
await page.locator('input[name=image]').fill(img_url)
|
||||
await page.locator('input[name=quote]').fill(quote)
|
||||
await page.locator('input[name=author]').fill(author)
|
||||
|
||||
await page.wait_for_timeout(500)
|
||||
await page.wait_for_load_state('networkidle')
|
||||
await page.wait_for_timeout(500)
|
||||
|
||||
out = await WebRenderer.render(
|
||||
konaweb('makequote'),
|
||||
target='#main',
|
||||
other_function=page_function,
|
||||
)
|
||||
await quote_cmd.send(await UniMessage().image(raw=out).export())
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ import imagetext_py
|
||||
import PIL.Image
|
||||
|
||||
from konabot.common.path import ASSETS_PATH
|
||||
from konabot.common.utils.to_async import make_async
|
||||
|
||||
from .base.fonts import HARMONYOS_SANS_SC_BLACK, HARMONYOS_SANS_SC_REGULAR, LXGWWENKAI_REGULAR
|
||||
|
||||
@ -14,6 +15,7 @@ mnk_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "mnksay.jpg").convert(
|
||||
dasuan_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "dss.png").convert("RGBA")
|
||||
suan_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "suanleba.png").convert("RGBA")
|
||||
cute_ten_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "tententen.png").convert("RGBA")
|
||||
kio_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "kiosay.jpg").convert("RGBA")
|
||||
|
||||
|
||||
def _draw_geimao(saying: str):
|
||||
@ -29,7 +31,7 @@ def _draw_geimao(saying: str):
|
||||
draw_emojis=True,
|
||||
)
|
||||
return img
|
||||
|
||||
|
||||
|
||||
async def draw_geimao(saying: str):
|
||||
return await asyncio.to_thread(_draw_geimao, saying)
|
||||
@ -106,3 +108,18 @@ def _draw_cute_ten(saying: str):
|
||||
|
||||
async def draw_cute_ten(saying: str):
|
||||
return await asyncio.to_thread(_draw_cute_ten, saying)
|
||||
|
||||
|
||||
@make_async
|
||||
def draw_kiosay(saying: str):
|
||||
img = kio_image.copy()
|
||||
with imagetext_py.Writer(img) as iw:
|
||||
iw.draw_text_wrapped(
|
||||
saying, 450, 540, 0.5, 0.5, 900, 96, LXGWWENKAI_REGULAR,
|
||||
imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")),
|
||||
1.0,
|
||||
imagetext_py.TextAlign.Center,
|
||||
draw_emojis=True,
|
||||
)
|
||||
return img
|
||||
|
||||
|
||||
196
poetry.lock
generated
196
poetry.lock
generated
@ -803,6 +803,23 @@ type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "distro"
|
||||
version = "1.9.0"
|
||||
description = "Distro - an OS platform information API"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"},
|
||||
{file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"},
|
||||
]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.3.0"
|
||||
@ -1333,6 +1350,123 @@ type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "jiter"
|
||||
version = "0.11.1"
|
||||
description = "Fast iterable JSON parser."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "jiter-0.11.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ed58841a491bbbf3f7c55a6b68fff568439ab73b2cce27ace0e169057b5851df"},
|
||||
{file = "jiter-0.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:499beb9b2d7e51d61095a8de39ebcab1d1778f2a74085f8305a969f6cee9f3e4"},
|
||||
{file = "jiter-0.11.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b87b2821795e28cc990939b68ce7a038edea680a24910bd68a79d54ff3f03c02"},
|
||||
{file = "jiter-0.11.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:83f6fa494d8bba14ab100417c80e70d32d737e805cb85be2052d771c76fcd1f8"},
|
||||
{file = "jiter-0.11.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fbc6aea1daa2ec6f5ed465f0c5e7b0607175062ceebbea5ca70dd5ddab58083"},
|
||||
{file = "jiter-0.11.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:302288e2edc43174bb2db838e94688d724f9aad26c5fb9a74f7a5fb427452a6a"},
|
||||
{file = "jiter-0.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85db563fe3b367bb568af5d29dea4d4066d923b8e01f3417d25ebecd958de815"},
|
||||
{file = "jiter-0.11.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f1c1ba2b6b22f775444ef53bc2d5778396d3520abc7b2e1da8eb0c27cb3ffb10"},
|
||||
{file = "jiter-0.11.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:523be464b14f8fd0cc78da6964b87b5515a056427a2579f9085ce30197a1b54a"},
|
||||
{file = "jiter-0.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25b99b3f04cd2a38fefb22e822e35eb203a2cd37d680dbbc0c0ba966918af336"},
|
||||
{file = "jiter-0.11.1-cp310-cp310-win32.whl", hash = "sha256:47a79e90545a596bb9104109777894033347b11180d4751a216afef14072dbe7"},
|
||||
{file = "jiter-0.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:cace75621ae9bd66878bf69fbd4dfc1a28ef8661e0c2d0eb72d3d6f1268eddf5"},
|
||||
{file = "jiter-0.11.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9b0088ff3c374ce8ce0168523ec8e97122ebb788f950cf7bb8e39c7dc6a876a2"},
|
||||
{file = "jiter-0.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74433962dd3c3090655e02e461267095d6c84f0741c7827de11022ef8d7ff661"},
|
||||
{file = "jiter-0.11.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d98030e345e6546df2cc2c08309c502466c66c4747b043f1a0d415fada862b8"},
|
||||
{file = "jiter-0.11.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d6db0b2e788db46bec2cf729a88b6dd36959af2abd9fa2312dfba5acdd96dcb"},
|
||||
{file = "jiter-0.11.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55678fbbda261eafe7289165dd2ddd0e922df5f9a1ae46d7c79a5a15242bd7d1"},
|
||||
{file = "jiter-0.11.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a6b74fae8e40497653b52ce6ca0f1b13457af769af6fb9c1113efc8b5b4d9be"},
|
||||
{file = "jiter-0.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a55a453f8b035eb4f7852a79a065d616b7971a17f5e37a9296b4b38d3b619e4"},
|
||||
{file = "jiter-0.11.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2638148099022e6bdb3f42904289cd2e403609356fb06eb36ddec2d50958bc29"},
|
||||
{file = "jiter-0.11.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:252490567a5d990986f83b95a5f1ca1bf205ebd27b3e9e93bb7c2592380e29b9"},
|
||||
{file = "jiter-0.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d431d52b0ca2436eea6195f0f48528202100c7deda354cb7aac0a302167594d5"},
|
||||
{file = "jiter-0.11.1-cp311-cp311-win32.whl", hash = "sha256:db6f41e40f8bae20c86cb574b48c4fd9f28ee1c71cb044e9ec12e78ab757ba3a"},
|
||||
{file = "jiter-0.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0cc407b8e6cdff01b06bb80f61225c8b090c3df108ebade5e0c3c10993735b19"},
|
||||
{file = "jiter-0.11.1-cp311-cp311-win_arm64.whl", hash = "sha256:fe04ea475392a91896d1936367854d346724a1045a247e5d1c196410473b8869"},
|
||||
{file = "jiter-0.11.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c92148eec91052538ce6823dfca9525f5cfc8b622d7f07e9891a280f61b8c96c"},
|
||||
{file = "jiter-0.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ecd4da91b5415f183a6be8f7158d127bdd9e6a3174138293c0d48d6ea2f2009d"},
|
||||
{file = "jiter-0.11.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7e3ac25c00b9275684d47aa42febaa90a9958e19fd1726c4ecf755fbe5e553b"},
|
||||
{file = "jiter-0.11.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:57d7305c0a841858f866cd459cd9303f73883fb5e097257f3d4a3920722c69d4"},
|
||||
{file = "jiter-0.11.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e86fa10e117dce22c547f31dd6d2a9a222707d54853d8de4e9a2279d2c97f239"},
|
||||
{file = "jiter-0.11.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae5ef1d48aec7e01ee8420155d901bb1d192998fa811a65ebb82c043ee186711"},
|
||||
{file = "jiter-0.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb68e7bf65c990531ad8715e57d50195daf7c8e6f1509e617b4e692af1108939"},
|
||||
{file = "jiter-0.11.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43b30c8154ded5845fa454ef954ee67bfccce629b2dea7d01f795b42bc2bda54"},
|
||||
{file = "jiter-0.11.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:586cafbd9dd1f3ce6a22b4a085eaa6be578e47ba9b18e198d4333e598a91db2d"},
|
||||
{file = "jiter-0.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:677cc2517d437a83bb30019fd4cf7cad74b465914c56ecac3440d597ac135250"},
|
||||
{file = "jiter-0.11.1-cp312-cp312-win32.whl", hash = "sha256:fa992af648fcee2b850a3286a35f62bbbaeddbb6dbda19a00d8fbc846a947b6e"},
|
||||
{file = "jiter-0.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:88b5cae9fa51efeb3d4bd4e52bfd4c85ccc9cac44282e2a9640893a042ba4d87"},
|
||||
{file = "jiter-0.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:9a6cae1ab335551917f882f2c3c1efe7617b71b4c02381e4382a8fc80a02588c"},
|
||||
{file = "jiter-0.11.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:71b6a920a5550f057d49d0e8bcc60945a8da998019e83f01adf110e226267663"},
|
||||
{file = "jiter-0.11.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b3de72e925388453a5171be83379549300db01284f04d2a6f244d1d8de36f94"},
|
||||
{file = "jiter-0.11.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc19dd65a2bd3d9c044c5b4ebf657ca1e6003a97c0fc10f555aa4f7fb9821c00"},
|
||||
{file = "jiter-0.11.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d58faaa936743cd1464540562f60b7ce4fd927e695e8bc31b3da5b914baa9abd"},
|
||||
{file = "jiter-0.11.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:902640c3103625317291cb73773413b4d71847cdf9383ba65528745ff89f1d14"},
|
||||
{file = "jiter-0.11.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30405f726e4c2ed487b176c09f8b877a957f535d60c1bf194abb8dadedb5836f"},
|
||||
{file = "jiter-0.11.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3217f61728b0baadd2551844870f65219ac4a1285d5e1a4abddff3d51fdabe96"},
|
||||
{file = "jiter-0.11.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1364cc90c03a8196f35f396f84029f12abe925415049204446db86598c8b72c"},
|
||||
{file = "jiter-0.11.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:53a54bf8e873820ab186b2dca9f6c3303f00d65ae5e7b7d6bda1b95aa472d646"},
|
||||
{file = "jiter-0.11.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7e29aca023627b0e0c2392d4248f6414d566ff3974fa08ff2ac8dbb96dfee92a"},
|
||||
{file = "jiter-0.11.1-cp313-cp313-win32.whl", hash = "sha256:f153e31d8bca11363751e875c0a70b3d25160ecbaee7b51e457f14498fb39d8b"},
|
||||
{file = "jiter-0.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:f773f84080b667c69c4ea0403fc67bb08b07e2b7ce1ef335dea5868451e60fed"},
|
||||
{file = "jiter-0.11.1-cp313-cp313-win_arm64.whl", hash = "sha256:635ecd45c04e4c340d2187bcb1cea204c7cc9d32c1364d251564bf42e0e39c2d"},
|
||||
{file = "jiter-0.11.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d892b184da4d94d94ddb4031296931c74ec8b325513a541ebfd6dfb9ae89904b"},
|
||||
{file = "jiter-0.11.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa22c223a3041dacb2fcd37c70dfd648b44662b4a48e242592f95bda5ab09d58"},
|
||||
{file = "jiter-0.11.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330e8e6a11ad4980cd66a0f4a3e0e2e0f646c911ce047014f984841924729789"},
|
||||
{file = "jiter-0.11.1-cp313-cp313t-win_amd64.whl", hash = "sha256:09e2e386ebf298547ca3a3704b729471f7ec666c2906c5c26c1a915ea24741ec"},
|
||||
{file = "jiter-0.11.1-cp313-cp313t-win_arm64.whl", hash = "sha256:fe4a431c291157e11cee7c34627990ea75e8d153894365a3bc84b7a959d23ca8"},
|
||||
{file = "jiter-0.11.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:0fa1f70da7a8a9713ff8e5f75ec3f90c0c870be6d526aa95e7c906f6a1c8c676"},
|
||||
{file = "jiter-0.11.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:569ee559e5046a42feb6828c55307cf20fe43308e3ae0d8e9e4f8d8634d99944"},
|
||||
{file = "jiter-0.11.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f69955fa1d92e81987f092b233f0be49d4c937da107b7f7dcf56306f1d3fcce9"},
|
||||
{file = "jiter-0.11.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:090f4c9d4a825e0fcbd0a2647c9a88a0f366b75654d982d95a9590745ff0c48d"},
|
||||
{file = "jiter-0.11.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbf3d8cedf9e9d825233e0dcac28ff15c47b7c5512fdfe2e25fd5bbb6e6b0cee"},
|
||||
{file = "jiter-0.11.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2aa9b1958f9c30d3d1a558b75f0626733c60eb9b7774a86b34d88060be1e67fe"},
|
||||
{file = "jiter-0.11.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e42d1ca16590b768c5e7d723055acd2633908baacb3628dd430842e2e035aa90"},
|
||||
{file = "jiter-0.11.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5db4c2486a023820b701a17aec9c5a6173c5ba4393f26662f032f2de9c848b0f"},
|
||||
{file = "jiter-0.11.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:4573b78777ccfac954859a6eff45cbd9d281d80c8af049d0f1a3d9fc323d5c3a"},
|
||||
{file = "jiter-0.11.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7593ac6f40831d7961cb67633c39b9fef6689a211d7919e958f45710504f52d3"},
|
||||
{file = "jiter-0.11.1-cp314-cp314-win32.whl", hash = "sha256:87202ec6ff9626ff5f9351507def98fcf0df60e9a146308e8ab221432228f4ea"},
|
||||
{file = "jiter-0.11.1-cp314-cp314-win_amd64.whl", hash = "sha256:a5dd268f6531a182c89d0dd9a3f8848e86e92dfff4201b77a18e6b98aa59798c"},
|
||||
{file = "jiter-0.11.1-cp314-cp314-win_arm64.whl", hash = "sha256:5d761f863f912a44748a21b5c4979c04252588ded8d1d2760976d2e42cd8d991"},
|
||||
{file = "jiter-0.11.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2cc5a3965285ddc33e0cab933e96b640bc9ba5940cea27ebbbf6695e72d6511c"},
|
||||
{file = "jiter-0.11.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b572b3636a784c2768b2342f36a23078c8d3aa6d8a30745398b1bab58a6f1a8"},
|
||||
{file = "jiter-0.11.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad93e3d67a981f96596d65d2298fe8d1aa649deb5374a2fb6a434410ee11915e"},
|
||||
{file = "jiter-0.11.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a83097ce379e202dcc3fe3fc71a16d523d1ee9192c8e4e854158f96b3efe3f2f"},
|
||||
{file = "jiter-0.11.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7042c51e7fbeca65631eb0c332f90c0c082eab04334e7ccc28a8588e8e2804d9"},
|
||||
{file = "jiter-0.11.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a68d679c0e47649a61df591660507608adc2652442de7ec8276538ac46abe08"},
|
||||
{file = "jiter-0.11.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1b0da75dbf4b6ec0b3c9e604d1ee8beaf15bc046fff7180f7d89e3cdbd3bb51"},
|
||||
{file = "jiter-0.11.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:69dd514bf0fa31c62147d6002e5ca2b3e7ef5894f5ac6f0a19752385f4e89437"},
|
||||
{file = "jiter-0.11.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:bb31ac0b339efa24c0ca606febd8b77ef11c58d09af1b5f2be4c99e907b11111"},
|
||||
{file = "jiter-0.11.1-cp314-cp314t-win32.whl", hash = "sha256:b2ce0d6156a1d3ad41da3eec63b17e03e296b78b0e0da660876fccfada86d2f7"},
|
||||
{file = "jiter-0.11.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f4db07d127b54c4a2d43b4cf05ff0193e4f73e0dd90c74037e16df0b29f666e1"},
|
||||
{file = "jiter-0.11.1-cp314-cp314t-win_arm64.whl", hash = "sha256:28e4fdf2d7ebfc935523e50d1efa3970043cfaa161674fe66f9642409d001dfe"},
|
||||
{file = "jiter-0.11.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:baa99c8db49467527658bb479857344daf0a14dff909b7f6714579ac439d1253"},
|
||||
{file = "jiter-0.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:860fe55fa3b01ad0edf2adde1098247ff5c303d0121f9ce028c03d4f88c69502"},
|
||||
{file = "jiter-0.11.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:173dd349d99b6feaf5a25a6fbcaf3489a6f947708d808240587a23df711c67db"},
|
||||
{file = "jiter-0.11.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:14ac1dca837514cc946a6ac2c4995d9695303ecc754af70a3163d057d1a444ab"},
|
||||
{file = "jiter-0.11.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69af47de5f93a231d5b85f7372d3284a5be8edb4cc758f006ec5a1406965ac5e"},
|
||||
{file = "jiter-0.11.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:685f8b3abd3bbd3e06e4dfe2429ff87fd5d7a782701151af99b1fcbd80e31b2b"},
|
||||
{file = "jiter-0.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d04afa2d4e5526e54ae8a58feea953b1844bf6e3526bc589f9de68e86d0ea01"},
|
||||
{file = "jiter-0.11.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e92b927259035b50d8e11a8fdfe0ebd014d883e4552d37881643fa289a4bcf1"},
|
||||
{file = "jiter-0.11.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e7bd8be4fad8d4c5558b7801770cd2da6c072919c6f247cc5336edb143f25304"},
|
||||
{file = "jiter-0.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:121381a77a3c85987f3eba0d30ceaca9116f7463bedeec2fa79b2e7286b89b60"},
|
||||
{file = "jiter-0.11.1-cp39-cp39-win32.whl", hash = "sha256:160225407f6dfabdf9be1b44e22f06bc293a78a28ffa4347054698bd712dad06"},
|
||||
{file = "jiter-0.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:028e0d59bcdfa1079f8df886cdaefc6f515c27a5288dec956999260c7e4a7cfd"},
|
||||
{file = "jiter-0.11.1-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:e642b5270e61dd02265866398707f90e365b5db2eb65a4f30c789d826682e1f6"},
|
||||
{file = "jiter-0.11.1-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:464ba6d000585e4e2fd1e891f31f1231f497273414f5019e27c00a4b8f7a24ad"},
|
||||
{file = "jiter-0.11.1-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:055568693ab35e0bf3a171b03bb40b2dcb10352359e0ab9b5ed0da2bf1eb6f6f"},
|
||||
{file = "jiter-0.11.1-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0c69ea798d08a915ba4478113efa9e694971e410056392f4526d796f136d3fa"},
|
||||
{file = "jiter-0.11.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:0d4d6993edc83cf75e8c6828a8d6ce40a09ee87e38c7bfba6924f39e1337e21d"},
|
||||
{file = "jiter-0.11.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f78d151c83a87a6cf5461d5ee55bc730dd9ae227377ac6f115b922989b95f838"},
|
||||
{file = "jiter-0.11.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9022974781155cd5521d5cb10997a03ee5e31e8454c9d999dcdccd253f2353f"},
|
||||
{file = "jiter-0.11.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18c77aaa9117510d5bdc6a946baf21b1f0cfa58ef04d31c8d016f206f2118960"},
|
||||
{file = "jiter-0.11.1.tar.gz", hash = "sha256:849dcfc76481c0ea0099391235b7ca97d7279e0fa4c86005457ac7c88e8b76dc"},
|
||||
]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "linkify-it-py"
|
||||
version = "2.0.3"
|
||||
@ -2200,6 +2334,39 @@ type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "2.7.1"
|
||||
description = "The official Python library for the openai API"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "openai-2.7.1-py3-none-any.whl", hash = "sha256:2f2530354d94c59c614645a4662b9dab0a5b881c5cd767a8587398feac0c9021"},
|
||||
{file = "openai-2.7.1.tar.gz", hash = "sha256:df4d4a3622b2df3475ead8eb0fbb3c27fd1c070fa2e55d778ca4f40e0186c726"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = ">=3.5.0,<5"
|
||||
distro = ">=1.7.0,<2"
|
||||
httpx = ">=0.23.0,<1"
|
||||
jiter = ">=0.10.0,<1"
|
||||
pydantic = ">=1.9.0,<3"
|
||||
sniffio = "*"
|
||||
tqdm = ">4"
|
||||
typing-extensions = ">=4.11,<5"
|
||||
|
||||
[package.extras]
|
||||
aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.9)"]
|
||||
datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"]
|
||||
realtime = ["websockets (>=13,<16)"]
|
||||
voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "opencc"
|
||||
version = "1.1.9"
|
||||
@ -3387,6 +3554,33 @@ type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.67.1"
|
||||
description = "Fast, Extensible Progress Meter"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"},
|
||||
{file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[package.extras]
|
||||
dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"]
|
||||
discord = ["requests"]
|
||||
notebook = ["ipywidgets (>=6)"]
|
||||
slack = ["slack-sdk"]
|
||||
telegram = ["requests"]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
@ -3978,4 +4172,4 @@ reference = "mirrors"
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.12,<4.0"
|
||||
content-hash = "ec73430f70658a303c47e6f536ccb0863a475f7f25d5334c8766e6149075648c"
|
||||
content-hash = "dcb6567ccb9eb6357179dd8b8eaa5fb69373cef0e17d3a49c7c895d289c0d642"
|
||||
|
||||
@ -27,6 +27,7 @@ dependencies = [
|
||||
"nanoid (>=2.0.0,<3.0.0)",
|
||||
"opencc (>=1.1.9,<2.0.0)",
|
||||
"playwright (>=1.55.0,<2.0.0)",
|
||||
"openai (>=2.7.1,<3.0.0)",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
||||
Reference in New Issue
Block a user