Compare commits
21 Commits
v0.9.5
...
feature/LL
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c19c52d9f | |||
| a5f4ae9bdc | |||
| 9320815d3f | |||
| 795300cb83 | |||
| 0231aa04f4 | |||
| 01fe33eb9f | |||
| adfbac7d90 | |||
| 994c1412da | |||
| 8780dfec6f | |||
| 490d807e7a | |||
| fa208199ab | |||
| 38a17f42a3 | |||
| 37179fc4d7 | |||
| 56e0aabbf3 | |||
| ce2b7fd6f6 | |||
| b28f8f85a2 | |||
| 0acffea86d | |||
| 3e395f8a35 | |||
| 312e203bbe | |||
| f9deabfce0 | |||
| 7a20c3fe2f |
22
.drone.yml
22
.drone.yml
@ -38,6 +38,17 @@ steps:
|
|||||||
path: /var/run/docker.sock
|
path: /var/run/docker.sock
|
||||||
commands:
|
commands:
|
||||||
- docker run --rm gitea.service.jazzwhom.top/mttu-developers/konabot:nightly-${DRONE_COMMIT_SHA} python scripts/test_plugin_load.py
|
- docker run --rm gitea.service.jazzwhom.top/mttu-developers/konabot:nightly-${DRONE_COMMIT_SHA} python scripts/test_plugin_load.py
|
||||||
|
- name: 发送构建结果到 ntfy
|
||||||
|
image: parrazam/drone-ntfy
|
||||||
|
when:
|
||||||
|
status: [success, failure]
|
||||||
|
settings:
|
||||||
|
url: https://ntfy.service.jazzwhom.top
|
||||||
|
topic: drone_ci
|
||||||
|
tags:
|
||||||
|
- drone-ci
|
||||||
|
token:
|
||||||
|
from_secret: NTFY_TOKEN
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- name: docker-socket
|
- name: docker-socket
|
||||||
@ -74,6 +85,17 @@ steps:
|
|||||||
volumes:
|
volumes:
|
||||||
- name: docker-socket
|
- name: docker-socket
|
||||||
path: /var/run/docker.sock
|
path: /var/run/docker.sock
|
||||||
|
- name: 发送构建结果到 ntfy
|
||||||
|
image: parrazam/drone-ntfy
|
||||||
|
when:
|
||||||
|
status: [success, failure]
|
||||||
|
settings:
|
||||||
|
url: https://ntfy.service.jazzwhom.top
|
||||||
|
topic: drone_ci
|
||||||
|
tags:
|
||||||
|
- drone-ci
|
||||||
|
token:
|
||||||
|
from_secret: NTFY_TOKEN
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- name: docker-socket
|
- name: docker-socket
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"python.REPL.enableREPLSmartSend": false
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
# 此方 bot!
|
# konabot
|
||||||
|
|
||||||
在 MTTU 内部使用的 bot 一只。
|
在 MTTU 内部使用的 bot 一只。
|
||||||
|
|
||||||
@ -63,12 +63,16 @@ code .
|
|||||||
|
|
||||||
配置 `ENABLE_CONSOLE=false`
|
配置 `ENABLE_CONSOLE=false`
|
||||||
|
|
||||||
|
#### 配置并支持 LLM(大语言模型)
|
||||||
|
|
||||||
|
详见[LLM 配置文档](/docs/LLM.md)。
|
||||||
|
|
||||||
### 运行
|
### 运行
|
||||||
|
|
||||||
使用命令行手动启动 Bot:
|
使用命令行手动启动 Bot:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
poetry run watchfiles bot.main konabot
|
poetry run watchfiles bot.main . --filter scripts.watch_filter.filter
|
||||||
```
|
```
|
||||||
|
|
||||||
如果你不希望自动重载,只是想运行 Bot,可以直接运行:
|
如果你不希望自动重载,只是想运行 Bot,可以直接运行:
|
||||||
|
|||||||
BIN
assets/webpage/ac/assets/background.png
Normal file
BIN
assets/webpage/ac/assets/background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
76
assets/webpage/ac/index.html
Normal file
76
assets/webpage/ac/index.html
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>空调炸炸排行榜</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="box">
|
||||||
|
<div class="text">位居全球第 <span id="ranking" class="ranking">200</span>!</div>
|
||||||
|
<div class="text-2">您的群总共坏了 <span id="number" class="number">200</span> 台空调</div>
|
||||||
|
<img class="background" src="./assets/background.png" alt="空调炸炸排行榜">
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
<style>
|
||||||
|
.box {
|
||||||
|
position: relative;
|
||||||
|
width: 1024px;
|
||||||
|
}
|
||||||
|
.number {
|
||||||
|
font-size: 2em;
|
||||||
|
color: #ffdd00;
|
||||||
|
text-shadow: 3px 3px 6px rgba(0, 0, 0, 0.7);
|
||||||
|
font-weight: bold;
|
||||||
|
font-stretch: 50%;
|
||||||
|
max-width: 520px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
line-height: 0.8em;
|
||||||
|
}
|
||||||
|
.background {
|
||||||
|
width: 1024px;
|
||||||
|
}
|
||||||
|
.text {
|
||||||
|
position: absolute;
|
||||||
|
top: 125px;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 72px;
|
||||||
|
color: white;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
|
||||||
|
font-weight: bolder;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.text-2 {
|
||||||
|
position: absolute;
|
||||||
|
top: 50px;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 48px;
|
||||||
|
color: white;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
|
||||||
|
font-weight: bolder;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.ranking {
|
||||||
|
font-size: 2em;
|
||||||
|
color: #ff0000;
|
||||||
|
-webkit-text-stroke: #ffffff 2px;
|
||||||
|
text-shadow: 3px 3px 6px rgba(0, 0, 0, 0.7);
|
||||||
|
font-weight: bold;
|
||||||
|
font-stretch: 50%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
// 从 URL 参数中获取 number 的值
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const number = urlParams.get('number');
|
||||||
|
// 将 number 显示在页面上
|
||||||
|
document.getElementById('number').textContent = number;
|
||||||
|
// 从 URL 参数中获取 ranking 的值
|
||||||
|
const ranking = urlParams.get('ranking');
|
||||||
|
// 将 ranking 显示在页面上
|
||||||
|
document.getElementById('ranking').textContent = ranking;
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
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 越狱等情况。
|
||||||
@ -19,12 +19,12 @@ class DataManager(Generic[T]):
|
|||||||
if not self.fp.exists():
|
if not self.fp.exists():
|
||||||
return self.cls()
|
return self.cls()
|
||||||
try:
|
try:
|
||||||
return self.cls.model_validate_json(self.fp.read_text())
|
return self.cls.model_validate_json(self.fp.read_text("utf-8"))
|
||||||
except ValidationError:
|
except ValidationError:
|
||||||
return self.cls()
|
return self.cls()
|
||||||
|
|
||||||
def save(self, data: T):
|
def save(self, data: T):
|
||||||
self.fp.write_text(data.model_dump_json())
|
self.fp.write_text(data.model_dump_json(), "utf-8")
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def get_data(self):
|
async def get_data(self):
|
||||||
|
|||||||
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]
|
||||||
|
|
||||||
@ -51,6 +51,10 @@ class LongTaskTarget(BaseModel):
|
|||||||
target_id: str
|
target_id: str
|
||||||
"沟通对象的 ID"
|
"沟通对象的 ID"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_private_chat(self):
|
||||||
|
return self.channel_id.startswith(QQ_PRIVATE_CHAT_CHANNEL_PREFIX)
|
||||||
|
|
||||||
async def send_message(self, msg: UniMessage | str, at: bool = True) -> bool:
|
async def send_message(self, msg: UniMessage | str, at: bool = True) -> bool:
|
||||||
try:
|
try:
|
||||||
bot = nonebot.get_bot(self.self_id)
|
bot = nonebot.get_bot(self.self_id)
|
||||||
@ -236,7 +240,7 @@ def handle_long_task(callback_id: str):
|
|||||||
|
|
||||||
def _load_longtask_data() -> LongTaskModuleData:
|
def _load_longtask_data() -> LongTaskModuleData:
|
||||||
try:
|
try:
|
||||||
txt = LONGTASK_DATA_DIR.read_text()
|
txt = LONGTASK_DATA_DIR.read_text("utf-8")
|
||||||
return LongTaskModuleData.model_validate_json(txt)
|
return LongTaskModuleData.model_validate_json(txt)
|
||||||
except (FileNotFoundError, ValidationError) as e:
|
except (FileNotFoundError, ValidationError) as e:
|
||||||
logger.info(f"取得 LongTask 数据时出现问题:{e}")
|
logger.info(f"取得 LongTask 数据时出现问题:{e}")
|
||||||
@ -247,7 +251,7 @@ def _load_longtask_data() -> LongTaskModuleData:
|
|||||||
|
|
||||||
|
|
||||||
def _save_longtask_data(data: LongTaskModuleData):
|
def _save_longtask_data(data: LongTaskModuleData):
|
||||||
LONGTASK_DATA_DIR.write_text(data.model_dump_json())
|
LONGTASK_DATA_DIR.write_text(data.model_dump_json(), "utf-8")
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
import nonebot
|
import nonebot
|
||||||
from nonebot_plugin_alconna import UniMessage
|
|
||||||
from nonebot.adapters.onebot.v11 import Bot as OBBot
|
from nonebot.adapters.onebot.v11 import Bot as OBBot
|
||||||
|
from nonebot_plugin_alconna import UniMessage
|
||||||
|
|
||||||
|
|
||||||
async def qq_broadcast(groups: list[str], msg: UniMessage[Any]):
|
async def qq_broadcast(groups: list[str], msg: UniMessage[Any] | str):
|
||||||
|
if isinstance(msg, str):
|
||||||
|
msg = UniMessage.text(msg)
|
||||||
bots: dict[str, OBBot] = {}
|
bots: dict[str, OBBot] = {}
|
||||||
|
|
||||||
# group_id -> bot_id
|
# group_id -> bot_id
|
||||||
|
|||||||
@ -6,6 +6,7 @@ FONTS_PATH = ASSETS_PATH / "fonts"
|
|||||||
SRC_PATH = Path(__file__).resolve().parent.parent
|
SRC_PATH = Path(__file__).resolve().parent.parent
|
||||||
DATA_PATH = SRC_PATH.parent / "data"
|
DATA_PATH = SRC_PATH.parent / "data"
|
||||||
LOG_PATH = DATA_PATH / "logs"
|
LOG_PATH = DATA_PATH / "logs"
|
||||||
|
CONFIG_PATH = DATA_PATH / "config"
|
||||||
|
|
||||||
DOCS_PATH = SRC_PATH / "docs"
|
DOCS_PATH = SRC_PATH / "docs"
|
||||||
DOCS_PATH_MAN1 = DOCS_PATH / "user"
|
DOCS_PATH_MAN1 = DOCS_PATH / "user"
|
||||||
@ -19,3 +20,5 @@ if not DATA_PATH.exists():
|
|||||||
if not LOG_PATH.exists():
|
if not LOG_PATH.exists():
|
||||||
LOG_PATH.mkdir()
|
LOG_PATH.mkdir()
|
||||||
|
|
||||||
|
CONFIG_PATH.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
|||||||
54
konabot/common/username.py
Normal file
54
konabot/common/username.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import re
|
||||||
|
import nonebot
|
||||||
|
|
||||||
|
from nonebot.adapters.onebot.v11 import Bot as OBBot
|
||||||
|
|
||||||
|
|
||||||
|
class UsernameManager:
|
||||||
|
grouped_data: dict[int, dict[int, str]]
|
||||||
|
individual_data: dict[int, str]
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.grouped_data = {}
|
||||||
|
self.individual_data = {}
|
||||||
|
|
||||||
|
async def update(self):
|
||||||
|
for bot in nonebot.get_bots().values():
|
||||||
|
if isinstance(bot, OBBot):
|
||||||
|
for user in await bot.get_friend_list():
|
||||||
|
uid = user["user_id"]
|
||||||
|
nickname = user["nickname"]
|
||||||
|
self.individual_data[uid] = nickname
|
||||||
|
for group in await bot.get_group_list():
|
||||||
|
gid = group["group_id"]
|
||||||
|
for member in await bot.get_group_member_list(group_id=gid):
|
||||||
|
uid = member["user_id"]
|
||||||
|
card = member.get("card", "")
|
||||||
|
nickname = member.get("nickname", "")
|
||||||
|
if card:
|
||||||
|
self.grouped_data.setdefault(gid, {})[uid] = card
|
||||||
|
if nickname:
|
||||||
|
self.individual_data[uid] = nickname
|
||||||
|
|
||||||
|
def get(self, qqid: int, groupid: int | None = None) -> str:
|
||||||
|
if groupid is not None and groupid in self.grouped_data:
|
||||||
|
n = self.grouped_data[groupid].get(qqid)
|
||||||
|
if n is not None:
|
||||||
|
return n
|
||||||
|
if qqid in self.individual_data:
|
||||||
|
return self.individual_data[qqid]
|
||||||
|
return str(qqid)
|
||||||
|
|
||||||
|
|
||||||
|
manager = UsernameManager()
|
||||||
|
|
||||||
|
def get_username(qqid: int | str, group: int | str | None = None):
|
||||||
|
if isinstance(group, str):
|
||||||
|
group = None if not re.match(r"^\d+$", group) else int(group)
|
||||||
|
if isinstance(qqid, str):
|
||||||
|
if re.match(r"^\d+$", qqid):
|
||||||
|
qqid = int(qqid)
|
||||||
|
else:
|
||||||
|
return qqid
|
||||||
|
return manager.get(qqid, group)
|
||||||
|
|
||||||
@ -80,6 +80,30 @@ class WebRenderer:
|
|||||||
page = cls.page_pool[page_id]
|
page = cls.page_pool[page_id]
|
||||||
return await instance.render_with_page(page, url, target, params=params, other_function=other_function, timeout=timeout)
|
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
|
@classmethod
|
||||||
async def close_persistent_page(cls, page_id: str) -> None:
|
async def close_persistent_page(cls, page_id: str) -> None:
|
||||||
'''
|
'''
|
||||||
@ -154,6 +178,10 @@ class WebRendererInstance:
|
|||||||
async with self.lock:
|
async with self.lock:
|
||||||
screenshot = await self.inner_render(page, url, target, index, params, other_function, timeout)
|
screenshot = await self.inner_render(page, url, target, index, params, other_function, timeout)
|
||||||
return screenshot
|
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:
|
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}")
|
logger.debug(f"Navigating to {url} with timeout {timeout}")
|
||||||
|
|||||||
11
konabot/docs/concepts/中间答案.txt
Normal file
11
konabot/docs/concepts/中间答案.txt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
关于「中间答案」或者「提示」:
|
||||||
|
|
||||||
|
在 KonaPH 中,当有人发送「提交答案 答案」时,会检查答案是否符合你设置的中间答案的 pattern。这个 pattern 可以有两种方式:
|
||||||
|
|
||||||
|
- 纯文本的完整匹配:你设置的 pattern 如果和提交的答案完全相等,则会触发提示。
|
||||||
|
- regex 匹配:你设置的 pattern 如果以斜杠(/)开头和结尾,就会检查提交的答案是否匹配正则表达式。注意 ^ 和 $ 符号的使用。
|
||||||
|
- 例如:/^commit$/ 会匹配 commit,不会匹配 acommit、Commit 等。
|
||||||
|
- 而如果是 /commit/,则会匹配 commit、acommit,而不会匹配 Commit。
|
||||||
|
- 无法使用 Javascript 的字符串声明模式,例如,/case_insensitive/i 就不会被视作一个正则表达式。
|
||||||
|
|
||||||
|
一个提示是提示,还是中间答案,取决于它是否有 checkpoint 标记。如果有 checkpoint 标记,则会提示用户「你回答了一个中间答案」,并且这个中间答案的回答会在排行榜中显示。
|
||||||
@ -1,13 +1,19 @@
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
|
import cv2
|
||||||
from nonebot.adapters import Event as BaseEvent
|
from nonebot.adapters import Event as BaseEvent
|
||||||
from nonebot.adapters.console.event import MessageEvent as ConsoleMessageEvent
|
from nonebot.adapters.console.event import MessageEvent as ConsoleMessageEvent
|
||||||
from nonebot.adapters.discord.event import MessageEvent as DiscordMessageEvent
|
from nonebot.adapters.discord.event import MessageEvent as DiscordMessageEvent
|
||||||
from nonebot_plugin_alconna import Alconna, AlconnaMatcher, Args, UniMessage, on_alconna
|
from nonebot_plugin_alconna import Alconna, AlconnaMatcher, Args, UniMessage, on_alconna
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
import numpy as np
|
||||||
from konabot.common.longtask import DepLongTaskTarget
|
from konabot.common.longtask import DepLongTaskTarget
|
||||||
from konabot.common.path import ASSETS_PATH
|
from konabot.common.path import ASSETS_PATH
|
||||||
from konabot.plugins.air_conditioner.ac import AirConditioner, generate_ac_image
|
from konabot.common.web_render import WebRenderer
|
||||||
|
from konabot.plugins.air_conditioner.ac import AirConditioner, CrashType, generate_ac_image, wiggle_transform
|
||||||
|
|
||||||
|
import random
|
||||||
|
import math
|
||||||
|
|
||||||
def get_ac(id: str) -> AirConditioner:
|
def get_ac(id: str) -> AirConditioner:
|
||||||
ac = AirConditioner.air_conditioners.get(id)
|
ac = AirConditioner.air_conditioners.get(id)
|
||||||
@ -70,42 +76,58 @@ async def _(event: BaseEvent, target: DepLongTaskTarget):
|
|||||||
await send_ac_image(evt, ac)
|
await send_ac_image(evt, ac)
|
||||||
|
|
||||||
evt = on_alconna(Alconna(
|
evt = on_alconna(Alconna(
|
||||||
"空调升温"
|
"空调升温",
|
||||||
|
Args["temp?", Optional[Union[int, float]]] # 可选参数,升温的度数,默认为1
|
||||||
), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=True)
|
), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=True)
|
||||||
|
|
||||||
@evt.handle()
|
@evt.handle()
|
||||||
async def _(event: BaseEvent, target: DepLongTaskTarget):
|
async def _(event: BaseEvent, target: DepLongTaskTarget, temp: Optional[Union[int, float]] = 1):
|
||||||
|
if temp <= 0:
|
||||||
|
return
|
||||||
id = target.channel_id
|
id = target.channel_id
|
||||||
ac = get_ac(id)
|
ac = get_ac(id)
|
||||||
if not ac.on or ac.burnt == True or ac.frozen == True:
|
if not ac.on or ac.burnt == True or ac.frozen == True:
|
||||||
await send_ac_image(evt, ac)
|
await send_ac_image(evt, ac)
|
||||||
return
|
return
|
||||||
ac.temperature += 1
|
ac.temperature += temp
|
||||||
if ac.temperature > 40:
|
if ac.temperature > 40:
|
||||||
# 打开爆炸图片
|
# 根据温度随机出是否爆炸,40度开始,呈指数增长
|
||||||
with open(ASSETS_PATH / "img" / "other" / "boom.jpg", "rb") as f:
|
possibility = -math.e ** ((40-ac.temperature) / 50) + 1
|
||||||
output = BytesIO()
|
if random.random() < possibility:
|
||||||
Image.open(f).save(output, format="GIF")
|
# 打开爆炸图片
|
||||||
await evt.send(await UniMessage().image(raw=output).export())
|
with open(ASSETS_PATH / "img" / "other" / "boom.jpg", "rb") as f:
|
||||||
ac.burnt = True
|
output = BytesIO()
|
||||||
await evt.send("太热啦,空调炸了!")
|
# 爆炸抖动
|
||||||
return
|
frames = wiggle_transform(np.array(Image.open(f)), intensity=5)
|
||||||
|
pil_frames = [Image.fromarray(frame) for frame in frames]
|
||||||
|
pil_frames[0].save(output, format="GIF", save_all=True, append_images=pil_frames[1:], loop=0, duration=35, disposal=2)
|
||||||
|
output.seek(0)
|
||||||
|
await evt.send(await UniMessage().image(raw=output).export())
|
||||||
|
ac.broke_ac(CrashType.BURNT)
|
||||||
|
await evt.send("太热啦,空调炸了!")
|
||||||
|
return
|
||||||
await send_ac_image(evt, ac)
|
await send_ac_image(evt, ac)
|
||||||
|
|
||||||
evt = on_alconna(Alconna(
|
evt = on_alconna(Alconna(
|
||||||
"空调降温"
|
"空调降温",
|
||||||
|
Args["temp?", Optional[Union[int, float]]] # 可选参数,降温的度数,默认为1
|
||||||
), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=True)
|
), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=True)
|
||||||
|
|
||||||
@evt.handle()
|
@evt.handle()
|
||||||
async def _(event: BaseEvent, target: DepLongTaskTarget):
|
async def _(event: BaseEvent, target: DepLongTaskTarget, temp: Optional[Union[int, float]] = 1):
|
||||||
|
if temp <= 0:
|
||||||
|
return
|
||||||
id = target.channel_id
|
id = target.channel_id
|
||||||
ac = get_ac(id)
|
ac = get_ac(id)
|
||||||
if not ac.on or ac.burnt == True or ac.frozen == True:
|
if not ac.on or ac.burnt == True or ac.frozen == True:
|
||||||
await send_ac_image(evt, ac)
|
await send_ac_image(evt, ac)
|
||||||
return
|
return
|
||||||
ac.temperature -= 1
|
ac.temperature -= temp
|
||||||
if ac.temperature < 0:
|
if ac.temperature < 0:
|
||||||
ac.frozen = True
|
# 根据温度随机出是否冻结,0度开始,呈指数增长
|
||||||
|
possibility = -math.e ** (ac.temperature / 50) + 1
|
||||||
|
if random.random() < possibility:
|
||||||
|
ac.broke_ac(CrashType.FROZEN)
|
||||||
await send_ac_image(evt, ac)
|
await send_ac_image(evt, ac)
|
||||||
|
|
||||||
evt = on_alconna(Alconna(
|
evt = on_alconna(Alconna(
|
||||||
@ -117,4 +139,24 @@ async def _(event: BaseEvent, target: DepLongTaskTarget):
|
|||||||
id = target.channel_id
|
id = target.channel_id
|
||||||
ac = get_ac(id)
|
ac = get_ac(id)
|
||||||
ac.change_ac()
|
ac.change_ac()
|
||||||
await send_ac_image(evt, ac)
|
await send_ac_image(evt, ac)
|
||||||
|
|
||||||
|
evt = on_alconna(Alconna(
|
||||||
|
"空调炸炸排行榜",
|
||||||
|
), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=True)
|
||||||
|
|
||||||
|
@evt.handle()
|
||||||
|
async def _(event: BaseEvent, target: DepLongTaskTarget):
|
||||||
|
id = target.channel_id
|
||||||
|
ac = get_ac(id)
|
||||||
|
number, ranking = ac.get_crashes_and_ranking()
|
||||||
|
params = {
|
||||||
|
"number": number,
|
||||||
|
"ranking": ranking
|
||||||
|
}
|
||||||
|
image = await WebRenderer.render_file(
|
||||||
|
file_path=ASSETS_PATH / "webpage" / "ac" / "index.html",
|
||||||
|
target=".box",
|
||||||
|
params=params
|
||||||
|
)
|
||||||
|
await evt.send(await UniMessage().image(raw=image).export())
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
from enum import Enum
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
@ -5,6 +6,12 @@ import numpy as np
|
|||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
from konabot.common.path import ASSETS_PATH, FONTS_PATH
|
from konabot.common.path import ASSETS_PATH, FONTS_PATH
|
||||||
|
from konabot.common.path import DATA_PATH
|
||||||
|
import json
|
||||||
|
|
||||||
|
class CrashType(Enum):
|
||||||
|
BURNT = 0
|
||||||
|
FROZEN = 1
|
||||||
|
|
||||||
class AirConditioner:
|
class AirConditioner:
|
||||||
air_conditioners: dict[str, "AirConditioner"] = {}
|
air_conditioners: dict[str, "AirConditioner"] = {}
|
||||||
@ -23,6 +30,60 @@ class AirConditioner:
|
|||||||
self.on = False
|
self.on = False
|
||||||
self.temperature = 24 # 重置为默认温度
|
self.temperature = 24 # 重置为默认温度
|
||||||
|
|
||||||
|
def broke_ac(self, crash_type: CrashType):
|
||||||
|
'''
|
||||||
|
让空调坏掉,并保存数据
|
||||||
|
|
||||||
|
:param crash_type: CrashType 枚举,表示空调坏掉的类型
|
||||||
|
'''
|
||||||
|
match crash_type:
|
||||||
|
case CrashType.BURNT:
|
||||||
|
self.burnt = True
|
||||||
|
case CrashType.FROZEN:
|
||||||
|
self.frozen = True
|
||||||
|
self.save_crash_data(crash_type)
|
||||||
|
|
||||||
|
def save_crash_data(self, crash_type: CrashType):
|
||||||
|
'''
|
||||||
|
如果空调爆炸了,就往本地的 ac_crash_data.json 里该 id 的记录加一
|
||||||
|
'''
|
||||||
|
data_file = DATA_PATH / "ac_crash_data.json"
|
||||||
|
crash_data = {}
|
||||||
|
if data_file.exists():
|
||||||
|
with open(data_file, "r", encoding="utf-8") as f:
|
||||||
|
crash_data = json.load(f)
|
||||||
|
if self.id not in crash_data:
|
||||||
|
crash_data[self.id] = {"burnt": 0, "frozen": 0}
|
||||||
|
match crash_type:
|
||||||
|
case CrashType.BURNT:
|
||||||
|
crash_data[self.id]["burnt"] += 1
|
||||||
|
case CrashType.FROZEN:
|
||||||
|
crash_data[self.id]["frozen"] += 1
|
||||||
|
with open(data_file, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(crash_data, f, ensure_ascii=False, indent=4)
|
||||||
|
|
||||||
|
def get_crashes_and_ranking(self) -> tuple[int, int]:
|
||||||
|
'''
|
||||||
|
获取该群在全国空调损坏的数量与排行榜的位置
|
||||||
|
'''
|
||||||
|
data_file = DATA_PATH / "ac_crash_data.json"
|
||||||
|
if not data_file.exists():
|
||||||
|
return 0, 1
|
||||||
|
with open(data_file, "r", encoding="utf-8") as f:
|
||||||
|
crash_data = json.load(f)
|
||||||
|
ranking_list = []
|
||||||
|
for gid, record in crash_data.items():
|
||||||
|
total = record.get("burnt", 0) + record.get("frozen", 0)
|
||||||
|
ranking_list.append((gid, total))
|
||||||
|
ranking_list.sort(key=lambda x: x[1], reverse=True)
|
||||||
|
total_crashes = crash_data.get(self.id, {}).get("burnt", 0) + crash_data.get(self.id, {}).get("frozen", 0)
|
||||||
|
rank = 1
|
||||||
|
for gid, total in ranking_list:
|
||||||
|
if gid == self.id:
|
||||||
|
break
|
||||||
|
rank += 1
|
||||||
|
return total_crashes, rank
|
||||||
|
|
||||||
def text_to_transparent_image(text, font_size=40, padding=0, text_color=(0, 0, 0)):
|
def text_to_transparent_image(text, font_size=40, padding=0, text_color=(0, 0, 0)):
|
||||||
"""
|
"""
|
||||||
将文本转换为带透明背景的图像,图像大小刚好包含文本
|
将文本转换为带透明背景的图像,图像大小刚好包含文本
|
||||||
@ -168,13 +229,13 @@ def precise_blend_with_perspective(background, foreground, corners):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def wiggle_transform(image) -> list[np.ndarray]:
|
def wiggle_transform(image, intensity=2) -> list[np.ndarray]:
|
||||||
'''
|
'''
|
||||||
返回一组图像振动的帧组,模拟空调运作时的抖动效果
|
返回一组图像振动的帧组,模拟空调运作时的抖动效果
|
||||||
'''
|
'''
|
||||||
frames = []
|
frames = []
|
||||||
height, width = image.shape[:2]
|
height, width = image.shape[:2]
|
||||||
shifts = [(-2, 0), (2, 0), (0, -2), (0, 2), (0, 0)]
|
shifts = [(-intensity, 0), (intensity, 0), (0, -intensity), (0, intensity), (0, 0)]
|
||||||
for dx, dy in shifts:
|
for dx, dy in shifts:
|
||||||
M = np.float32([[1, 0, dx], [0, 1, dy]])
|
M = np.float32([[1, 0, dx], [0, 1, dy]])
|
||||||
shifted = cv2.warpAffine(image, M, (width, height))
|
shifted = cv2.warpAffine(image, M, (width, height))
|
||||||
@ -193,7 +254,7 @@ async def generate_ac_image(ac: AirConditioner) -> BytesIO:
|
|||||||
return output
|
return output
|
||||||
|
|
||||||
# 根据生成温度文本图像
|
# 根据生成温度文本图像
|
||||||
text = f"{ac.temperature}°C"
|
text = f"{round(ac.temperature, 1)}°C"
|
||||||
text_image = text_to_transparent_image(
|
text_image = text_to_transparent_image(
|
||||||
text,
|
text,
|
||||||
font_size=60,
|
font_size=60,
|
||||||
@ -218,8 +279,10 @@ async def generate_ac_image(ac: AirConditioner) -> BytesIO:
|
|||||||
|
|
||||||
final_image_simple = blend_with_transparency(ac_image, transformed_text, (0, 0))
|
final_image_simple = blend_with_transparency(ac_image, transformed_text, (0, 0))
|
||||||
|
|
||||||
frames = wiggle_transform(final_image_simple)
|
intensity = max(2, abs(int(ac.temperature) - 24) // 2)
|
||||||
|
|
||||||
|
frames = wiggle_transform(final_image_simple, intensity=intensity)
|
||||||
pil_frames = [Image.fromarray(frame) for frame in frames]
|
pil_frames = [Image.fromarray(frame) for frame in frames]
|
||||||
output = BytesIO()
|
output = BytesIO()
|
||||||
pil_frames[0].save(output, format="GIF", save_all=True, append_images=pil_frames[1:], loop=0, duration=50)
|
pil_frames[0].save(output, format="GIF", save_all=True, append_images=pil_frames[1:], loop=0, duration=50, disposal=2)
|
||||||
return output
|
return output
|
||||||
@ -30,7 +30,7 @@ def load_banned_ids() -> list[str]:
|
|||||||
if not DATA_FILE_PATH.exists():
|
if not DATA_FILE_PATH.exists():
|
||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
return json.loads(DATA_FILE_PATH.read_text())
|
return json.loads(DATA_FILE_PATH.read_text("utf-8"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"在解析成语接龙封禁文件时遇到问题:{e}")
|
logger.warning(f"在解析成语接龙封禁文件时遇到问题:{e}")
|
||||||
return []
|
return []
|
||||||
@ -45,14 +45,14 @@ def add_banned_id(group_id: str):
|
|||||||
banned_ids = load_banned_ids()
|
banned_ids = load_banned_ids()
|
||||||
if group_id not in banned_ids:
|
if group_id not in banned_ids:
|
||||||
banned_ids.append(group_id)
|
banned_ids.append(group_id)
|
||||||
DATA_FILE_PATH.write_text(json.dumps(banned_ids, ensure_ascii=False, indent=4))
|
DATA_FILE_PATH.write_text(json.dumps(banned_ids, ensure_ascii=False, indent=4), "utf-8")
|
||||||
|
|
||||||
|
|
||||||
def remove_banned_id(group_id: str):
|
def remove_banned_id(group_id: str):
|
||||||
banned_ids = load_banned_ids()
|
banned_ids = load_banned_ids()
|
||||||
if group_id in banned_ids:
|
if group_id in banned_ids:
|
||||||
banned_ids.remove(group_id)
|
banned_ids.remove(group_id)
|
||||||
DATA_FILE_PATH.write_text(json.dumps(banned_ids, ensure_ascii=False, indent=4))
|
DATA_FILE_PATH.write_text(json.dumps(banned_ids, ensure_ascii=False, indent=4), "utf-8")
|
||||||
|
|
||||||
|
|
||||||
class TryStartState(Enum):
|
class TryStartState(Enum):
|
||||||
|
|||||||
@ -1,39 +1,57 @@
|
|||||||
|
import datetime
|
||||||
|
import re
|
||||||
from math import ceil
|
from math import ceil
|
||||||
from loguru import logger
|
|
||||||
from nonebot_plugin_alconna import Alconna, Args, UniMessage, on_alconna
|
|
||||||
from konabot.common.nb.qq_broadcast import qq_broadcast
|
|
||||||
from konabot.plugins.kona_ph.core.storage import get_today_date
|
|
||||||
from konabot.plugins.kona_ph.manager import PUZZLE_PAGE_SIZE, create_admin_commands, config, puzzle_manager
|
|
||||||
from konabot.common.longtask import DepLongTaskTarget
|
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
from nonebot import on_message
|
||||||
|
from nonebot_plugin_alconna import (Alconna, Args, UniMessage, UniMsg,
|
||||||
|
on_alconna)
|
||||||
from nonebot_plugin_apscheduler import scheduler
|
from nonebot_plugin_apscheduler import scheduler
|
||||||
|
|
||||||
|
from konabot.common.longtask import DepLongTaskTarget
|
||||||
|
from konabot.common.nb.qq_broadcast import qq_broadcast
|
||||||
|
from konabot.plugins.kona_ph.core.message import (get_daily_report,
|
||||||
|
get_daily_report_v2,
|
||||||
|
get_puzzle_description,
|
||||||
|
get_submission_message)
|
||||||
|
from konabot.plugins.kona_ph.core.storage import get_today_date
|
||||||
|
from konabot.plugins.kona_ph.manager import (PUZZLE_PAGE_SIZE, config,
|
||||||
|
create_admin_commands,
|
||||||
|
puzzle_manager)
|
||||||
|
|
||||||
create_admin_commands()
|
create_admin_commands()
|
||||||
|
|
||||||
|
|
||||||
async def is_play_group(target: DepLongTaskTarget):
|
async def is_play_group(target: DepLongTaskTarget):
|
||||||
if target.channel_id in config.plugin_puzzle_playgroup:
|
if target.is_private_chat:
|
||||||
return True
|
return True
|
||||||
if target.target_id in target.channel_id:
|
if target.channel_id in config.plugin_puzzle_playgroup:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
cmd_submit = on_alconna(Alconna(
|
cmd_submit = on_message(rule=is_play_group)
|
||||||
"re:提交(?:答案|题解|[fF]lag)",
|
|
||||||
Args["flag", str],
|
|
||||||
), rule=is_play_group)
|
|
||||||
|
|
||||||
@cmd_submit.handle()
|
@cmd_submit.handle()
|
||||||
async def _(flag: str, target: DepLongTaskTarget):
|
async def _(msg: UniMsg, target: DepLongTaskTarget):
|
||||||
async with puzzle_manager() as manager:
|
txt = msg.extract_plain_text().strip()
|
||||||
result = manager.submit(target.target_id, flag)
|
if match := re.match(r"^提交(?:答案|题解|[fF]lag)\s*(?P<submission>.+?)\s*$", txt):
|
||||||
await target.send_message(result.get_unimessage())
|
submission: str = match.group("submission")
|
||||||
|
async with puzzle_manager() as manager:
|
||||||
|
result = manager.submit(target.target_id, submission)
|
||||||
|
if isinstance(result, str):
|
||||||
|
await target.send_message(result)
|
||||||
|
else:
|
||||||
|
await target.send_message(get_submission_message(
|
||||||
|
daily_puzzle_info=result.info,
|
||||||
|
submission=result.submission,
|
||||||
|
puzzle=result.puzzle,
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
cmd_query = on_alconna(Alconna(
|
cmd_query = on_alconna(Alconna(
|
||||||
r"re:(?:(?:所以|话)说?)?今天的题目是什么[啊呀哇呢]?(?:\??)?"
|
r"re:(?:((?:(?:所以|话)说?)?今天的题目是什么[啊呀哇呢]?(?:\??)?)|今日谜?题目?)"
|
||||||
), rule=is_play_group)
|
), rule=is_play_group)
|
||||||
|
|
||||||
@cmd_query.handle()
|
@cmd_query.handle()
|
||||||
@ -42,7 +60,20 @@ async def _(target: DepLongTaskTarget):
|
|||||||
p = manager.get_today_puzzle()
|
p = manager.get_today_puzzle()
|
||||||
if p is None:
|
if p is None:
|
||||||
return await target.send_message("今天无题,改日再来吧!")
|
return await target.send_message("今天无题,改日再来吧!")
|
||||||
await target.send_message(p.get_unimessage())
|
await target.send_message(get_puzzle_description(p))
|
||||||
|
|
||||||
|
|
||||||
|
cmd_query_submission = on_alconna(Alconna(
|
||||||
|
"今日答题情况"
|
||||||
|
), rule=is_play_group)
|
||||||
|
|
||||||
|
@cmd_query_submission.handle()
|
||||||
|
async def _(target: DepLongTaskTarget):
|
||||||
|
gid = None
|
||||||
|
if re.match(r"^\d+$", target.channel_id):
|
||||||
|
gid = int(target.channel_id)
|
||||||
|
async with puzzle_manager() as manager:
|
||||||
|
await target.send_message(get_daily_report_v2(manager, gid))
|
||||||
|
|
||||||
|
|
||||||
cmd_history = on_alconna(Alconna(
|
cmd_history = on_alconna(Alconna(
|
||||||
@ -57,15 +88,14 @@ async def _(target: DepLongTaskTarget, index_id: str = "", page: int = 1):
|
|||||||
today = get_today_date()
|
today = get_today_date()
|
||||||
if index_id:
|
if index_id:
|
||||||
index_id = index_id.removeprefix("#")
|
index_id = index_id.removeprefix("#")
|
||||||
if index_id == manager.daily_puzzle_of_date.get(today, ""):
|
if index_id not in manager.daily_puzzle:
|
||||||
puzzle = manager.puzzle_data[manager.daily_puzzle[index_id].raw_id]
|
return await target.send_message("没有这道题哦")
|
||||||
return await target.send_message(puzzle.get_unimessage())
|
puzzle = manager.puzzle_data[manager.daily_puzzle[index_id].raw_id]
|
||||||
if index_id in manager.daily_puzzle:
|
msg = get_puzzle_description(
|
||||||
puzzle = manager.puzzle_data[manager.daily_puzzle[index_id].raw_id]
|
puzzle,
|
||||||
msg = puzzle.get_unimessage()
|
with_answer=(index_id != manager.daily_puzzle_of_date.get(today, "")),
|
||||||
msg = msg.text(f"\n\n------\n\n题解:{puzzle.flag}")
|
)
|
||||||
return await target.send_message(msg)
|
return await target.send_message(msg)
|
||||||
return await target.send_message("没有这道题哦")
|
|
||||||
msg = UniMessage.text("====== 历史题目清单 ======\n\n")
|
msg = UniMessage.text("====== 历史题目清单 ======\n\n")
|
||||||
puzzles = [
|
puzzles = [
|
||||||
(manager.puzzle_data[manager.daily_puzzle[i].raw_id], d)
|
(manager.puzzle_data[manager.daily_puzzle[i].raw_id], d)
|
||||||
@ -92,10 +122,14 @@ async def _(target: DepLongTaskTarget, index_id: str = "", page: int = 1):
|
|||||||
@scheduler.scheduled_job("cron", hour="8")
|
@scheduler.scheduled_job("cron", hour="8")
|
||||||
async def _():
|
async def _():
|
||||||
async with puzzle_manager() as manager:
|
async with puzzle_manager() as manager:
|
||||||
|
yesterday = get_today_date() - datetime.timedelta(days=1)
|
||||||
|
msg2 = get_daily_report(manager, yesterday)
|
||||||
|
if msg2 is not None:
|
||||||
|
await qq_broadcast(config.plugin_puzzle_playgroup, msg2)
|
||||||
|
|
||||||
puzzle = manager.get_today_puzzle()
|
puzzle = manager.get_today_puzzle()
|
||||||
if puzzle is not None:
|
if puzzle is not None:
|
||||||
logger.info(f"找到了题目 {puzzle.raw_id},发送")
|
logger.info(f"找到了题目 {puzzle.raw_id},发送")
|
||||||
await qq_broadcast(config.plugin_puzzle_playgroup, puzzle.get_unimessage())
|
await qq_broadcast(config.plugin_puzzle_playgroup, get_puzzle_description(puzzle))
|
||||||
else:
|
else:
|
||||||
logger.info("自动任务:没有找到题目,跳过")
|
logger.info("自动任务:没有找到题目,跳过")
|
||||||
|
|
||||||
|
|||||||
29
konabot/plugins/kona_ph/core/image.py
Normal file
29
konabot/plugins/kona_ph/core/image.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import nanoid
|
||||||
|
|
||||||
|
from konabot.common.path import ASSETS_PATH
|
||||||
|
from konabot.plugins.kona_ph.core.path import KONAPH_IMAGE_BASE
|
||||||
|
|
||||||
|
|
||||||
|
class PuzzleImageManager:
|
||||||
|
def read_puzzle_image(self, img_name: str) -> bytes:
|
||||||
|
fp = KONAPH_IMAGE_BASE / img_name
|
||||||
|
if fp.exists():
|
||||||
|
return fp.read_bytes()
|
||||||
|
return (ASSETS_PATH / "img" / "other" / "boom.jpg").read_bytes()
|
||||||
|
|
||||||
|
def upload_puzzle_image(self, data: bytes, suffix: str = ".png") -> str:
|
||||||
|
id = nanoid.generate(
|
||||||
|
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz",
|
||||||
|
21,
|
||||||
|
)
|
||||||
|
img_name = f"{id}{suffix}"
|
||||||
|
(KONAPH_IMAGE_BASE / img_name).write_bytes(data)
|
||||||
|
return img_name
|
||||||
|
|
||||||
|
def remove_puzzle_image(self, img_name: str):
|
||||||
|
if img_name:
|
||||||
|
(KONAPH_IMAGE_BASE / img_name).unlink(True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_image_manager() -> PuzzleImageManager:
|
||||||
|
return PuzzleImageManager()
|
||||||
187
konabot/plugins/kona_ph/core/message.py
Normal file
187
konabot/plugins/kona_ph/core/message.py
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
"""
|
||||||
|
生成各种各样的 Message 的函数集合
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import functools
|
||||||
|
import re
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from nonebot_plugin_alconna import UniMessage
|
||||||
|
|
||||||
|
from konabot.common.username import get_username
|
||||||
|
from konabot.plugins.kona_ph.core.image import get_image_manager
|
||||||
|
from konabot.plugins.kona_ph.core.storage import (DailyPuzzleInfo, Puzzle,
|
||||||
|
PuzzleManager,
|
||||||
|
PuzzleSubmission)
|
||||||
|
|
||||||
|
|
||||||
|
def get_puzzle_description(puzzle: Puzzle, with_answer: bool = False) -> UniMessage[Any]:
|
||||||
|
"""
|
||||||
|
获取一个谜题的描述
|
||||||
|
"""
|
||||||
|
|
||||||
|
img_manager = get_image_manager()
|
||||||
|
|
||||||
|
result = UniMessage.text(f"[KonaPH#{puzzle.index_id}] {puzzle.title}")
|
||||||
|
result = result.text(f"\n\n{puzzle.content}")
|
||||||
|
|
||||||
|
if puzzle.img_name:
|
||||||
|
result = result.text("\n\n").image(
|
||||||
|
raw=img_manager.read_puzzle_image(puzzle.img_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = result.text(f"\n\n出题者:{get_username(puzzle.author_id)}")
|
||||||
|
|
||||||
|
if with_answer:
|
||||||
|
result = result.text(f"\n\n题目答案:{puzzle.flag}")
|
||||||
|
else:
|
||||||
|
result = result.text("\n\n输入「提交答案 答案」来提交你的解答")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_submission_message(
|
||||||
|
puzzle: Puzzle,
|
||||||
|
submission: PuzzleSubmission,
|
||||||
|
daily_puzzle_info: DailyPuzzleInfo | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
获得提交答案的反馈信息
|
||||||
|
"""
|
||||||
|
|
||||||
|
if submission.success:
|
||||||
|
rank = -1
|
||||||
|
if daily_puzzle_info is not None:
|
||||||
|
rank = len(daily_puzzle_info.success_users)
|
||||||
|
return f"🎉 恭喜你答对了!你是今天第 {rank} 个解出来的!"
|
||||||
|
if submission.hint_id >= 0 and (
|
||||||
|
hint := puzzle.hints.get(submission.hint_id)
|
||||||
|
) is not None:
|
||||||
|
if hint.is_checkpoint:
|
||||||
|
hint_msg = "✨ 恭喜!这是本题的中间答案,加油!"
|
||||||
|
else:
|
||||||
|
hint_msg = "🤔 答错啦!请检查你的答案。"
|
||||||
|
return f"{hint_msg}\n\n提示:{hint.message}"
|
||||||
|
return "❌ 答错啦!请检查你的答案。"
|
||||||
|
|
||||||
|
|
||||||
|
def get_daily_report(
|
||||||
|
manager: PuzzleManager,
|
||||||
|
date: datetime.date,
|
||||||
|
) -> str | None:
|
||||||
|
"""
|
||||||
|
获得某日的提交的报告信息
|
||||||
|
"""
|
||||||
|
|
||||||
|
index_id = manager.daily_puzzle_of_date.get(date)
|
||||||
|
if index_id is None:
|
||||||
|
return None
|
||||||
|
info = manager.daily_puzzle[index_id]
|
||||||
|
puzzle = manager.puzzle_data[info.raw_id]
|
||||||
|
|
||||||
|
msg = f"[KonaPH#{puzzle.index_id}] 「{puzzle.title}」解答报告\n\n"
|
||||||
|
if len(info.success_users) == 0:
|
||||||
|
msg += "昨日,无人解出此题 😭😭\n\n"
|
||||||
|
else:
|
||||||
|
msg += f"昨日,共有 {len(info.success_users)} 人解出此题。\n\n"
|
||||||
|
msg += "前五名的解答者:\n\n"
|
||||||
|
us = [(u, d) for u, d in info.success_users.items()]
|
||||||
|
us = sorted(us, key=lambda t: t[1])
|
||||||
|
us = us[:5]
|
||||||
|
for u, _ in us:
|
||||||
|
m = manager.submissions[puzzle.raw_id][u][-1]
|
||||||
|
msg += f"- {get_username(u)} 于 {m.time.strftime('%H:%M')}\n"
|
||||||
|
msg += "\n"
|
||||||
|
msg += f"出题人:{get_username(puzzle.author_id)}"
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
def get_daily_report_v2(manager: PuzzleManager, gid: int | None = None):
|
||||||
|
p = manager.get_today_puzzle()
|
||||||
|
if p is None:
|
||||||
|
return "今天无题"
|
||||||
|
msg = "==== 今日答题情况 ====\n\n"
|
||||||
|
|
||||||
|
subcount = len(functools.reduce(
|
||||||
|
lambda x, y: x + y,
|
||||||
|
manager.submissions.get(p.raw_id, {}).values(),
|
||||||
|
[],
|
||||||
|
))
|
||||||
|
info = manager.daily_puzzle[p.index_id]
|
||||||
|
|
||||||
|
msg += (
|
||||||
|
f"总体情况:答对 {len(info.success_users)} / "
|
||||||
|
f"参与 {len(info.tried_users)} / "
|
||||||
|
f"提交 {subcount}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
success_users = sorted(list(info.success_users.items()), key=lambda v: v[1])
|
||||||
|
for u, d in success_users:
|
||||||
|
uname = u
|
||||||
|
if re.match(r"^\d+$", u):
|
||||||
|
uname = get_username(int(u), gid)
|
||||||
|
t = d.strftime("%H:%M")
|
||||||
|
tries = len(manager.submissions[p.raw_id][u])
|
||||||
|
msg += f"\n- {uname} [🎉 {t} 完成 | {tries} 提交]"
|
||||||
|
for u in info.tried_users - set(info.success_users.keys()):
|
||||||
|
uname = u
|
||||||
|
if re.match(r"^\d+$", u):
|
||||||
|
uname = get_username(int(u), gid)
|
||||||
|
tries = len(manager.submissions[p.raw_id][u])
|
||||||
|
checkpoints_touched = len(set((
|
||||||
|
s.hint_id for s in manager.submissions[p.raw_id][u]
|
||||||
|
if (
|
||||||
|
s.hint_id >= 0
|
||||||
|
and s.hint_id in p.hints
|
||||||
|
and p.hints[s.hint_id].is_checkpoint
|
||||||
|
)
|
||||||
|
)))
|
||||||
|
checkpoint_message = ""
|
||||||
|
if checkpoints_touched > 0:
|
||||||
|
checkpoint_message = f" | 🚩 {checkpoints_touched} 记录点"
|
||||||
|
msg += f"\n- {uname} [💦 {tries} 提交{checkpoint_message}]"
|
||||||
|
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
def get_puzzle_info_message(manager: PuzzleManager, puzzle: Puzzle) -> UniMessage[Any]:
|
||||||
|
image_manager = get_image_manager()
|
||||||
|
|
||||||
|
status = "✅ 已准备,待发布" if puzzle.ready and not manager.is_puzzle_published(puzzle.raw_id) else \
|
||||||
|
(f"🟢 已发布: #{puzzle.index_id}" if manager.is_puzzle_published(puzzle.raw_id) else "⚙️ 未准备")
|
||||||
|
|
||||||
|
status_suffix = ""
|
||||||
|
if puzzle.raw_id == manager.puzzle_pinned:
|
||||||
|
status_suffix += " | 📌 已被管理员置顶"
|
||||||
|
|
||||||
|
msg = UniMessage.text(
|
||||||
|
f"--- 谜题信息 ---\n"
|
||||||
|
f"Raw ID: {puzzle.raw_id}\n"
|
||||||
|
f"出题者: {get_username(puzzle.author_id)} | {puzzle.author_id}\n"
|
||||||
|
f"创建时间: {puzzle.created_at.strftime('%Y-%m-%d %H:%M:%S')}\n"
|
||||||
|
f"状态: {status}{status_suffix}\n\n"
|
||||||
|
f"标题: {puzzle.title}\n"
|
||||||
|
f"Flag: {puzzle.flag}\n\n"
|
||||||
|
f"{puzzle.content}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if puzzle.img_name:
|
||||||
|
msg = msg.image(raw=image_manager.read_puzzle_image(puzzle.img_name))
|
||||||
|
|
||||||
|
msg = msg.text(f"\n---------\n使用 `konaph ready {puzzle.raw_id}` 完成编辑")
|
||||||
|
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
def get_puzzle_hint_list(puzzle: Puzzle) -> str:
|
||||||
|
msg = f"==== {puzzle.title} 提示与中间答案 ====\n"
|
||||||
|
if len(puzzle.hints) == 0:
|
||||||
|
msg += "\n你没有添加任何中间答案。"
|
||||||
|
return msg
|
||||||
|
for hint_id, hint in puzzle.hints.items():
|
||||||
|
n = {False: "[提示]", True: "[中间答案]"}[hint.is_checkpoint]
|
||||||
|
msg += f"\n{n}[{hint_id}] {hint.pattern}"
|
||||||
|
msg += f"\n {hint.message}"
|
||||||
|
return msg
|
||||||
9
konabot/plugins/kona_ph/core/path.py
Normal file
9
konabot/plugins/kona_ph/core/path.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from konabot.common.path import DATA_PATH
|
||||||
|
|
||||||
|
KONAPH_BASE = DATA_PATH / "KonaPH"
|
||||||
|
KONAPH_DATA_JSON = KONAPH_BASE / "data.json"
|
||||||
|
KONAPH_IMAGE_BASE = KONAPH_BASE / "imgs"
|
||||||
|
|
||||||
|
# 保证所有文件夹存在
|
||||||
|
KONAPH_BASE.mkdir(exist_ok=True)
|
||||||
|
KONAPH_IMAGE_BASE.mkdir(exist_ok=True)
|
||||||
@ -1,26 +1,26 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import nanoid
|
import nanoid
|
||||||
|
|
||||||
from nonebot_plugin_alconna import UniMessage
|
|
||||||
from pydantic import BaseModel, Field, ValidationError
|
from pydantic import BaseModel, Field, ValidationError
|
||||||
|
|
||||||
from konabot.common.path import DATA_PATH
|
from konabot.plugins.kona_ph.core.path import KONAPH_DATA_JSON
|
||||||
|
|
||||||
|
|
||||||
KONAPH_BASE = DATA_PATH / "KonaPH"
|
class PuzzleHint(BaseModel):
|
||||||
KONAPH_DATA_JSON = KONAPH_BASE / "data.json"
|
pattern: str
|
||||||
KONAPH_IMAGE_BASE = KONAPH_BASE / "imgs"
|
message: str
|
||||||
|
is_checkpoint: bool
|
||||||
|
|
||||||
# 保证所有文件夹存在
|
|
||||||
KONAPH_BASE.mkdir(exist_ok=True)
|
class PuzzleSubmission(BaseModel):
|
||||||
KONAPH_IMAGE_BASE.mkdir(exist_ok=True)
|
success: bool
|
||||||
|
flag: str
|
||||||
|
time: datetime.datetime
|
||||||
|
hint_id: int = -1
|
||||||
|
|
||||||
|
|
||||||
class Puzzle(BaseModel):
|
class Puzzle(BaseModel):
|
||||||
@ -37,46 +37,49 @@ class Puzzle(BaseModel):
|
|||||||
flag: str
|
flag: str
|
||||||
|
|
||||||
ready: bool = False
|
ready: bool = False
|
||||||
published: bool = False
|
|
||||||
pinned: bool = False
|
|
||||||
|
|
||||||
created_at: datetime.datetime = Field(default_factory=datetime.datetime.now)
|
created_at: datetime.datetime = Field(default_factory=datetime.datetime.now)
|
||||||
|
|
||||||
def get_image_path(self) -> Path:
|
hints: dict[int, PuzzleHint] = Field(default_factory=dict)
|
||||||
return KONAPH_IMAGE_BASE / self.img_name
|
|
||||||
|
|
||||||
def get_unimessage(self) -> UniMessage[Any]:
|
@property
|
||||||
result = UniMessage.text(f"[KonaPH#{self.index_id}] {self.title}")
|
def hint_id_max(self) -> int:
|
||||||
result = result.text(f"\n\n{self.content}")
|
return max((0, *self.hints.keys()))
|
||||||
|
|
||||||
if self.img_name:
|
def check_submission(
|
||||||
result = result.text("\n\n").image(raw=self.get_image_path().read_bytes())
|
self,
|
||||||
|
submission: str,
|
||||||
result = result.text("\n\n出题者:").at(self.author_id)
|
time: datetime.datetime | None = None,
|
||||||
result = result.text("\n\n输入「提交答案 答案」来提交你的解答")
|
) -> PuzzleSubmission:
|
||||||
|
if time is None:
|
||||||
return result
|
time = datetime.datetime.now()
|
||||||
|
if submission == self.flag:
|
||||||
def add_image(self, img: bytes, suffix: str = ".png"):
|
return PuzzleSubmission(
|
||||||
if self.img_name:
|
success=True,
|
||||||
self.get_image_path().unlink(True)
|
flag=submission,
|
||||||
img_id = nanoid.generate(
|
time=time,
|
||||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz",
|
)
|
||||||
21,
|
for hint_id, hint in self.hints.items():
|
||||||
|
if hint.pattern.startswith('/') and hint.pattern.endswith('/'):
|
||||||
|
if re.match(hint.pattern.strip('/'), submission):
|
||||||
|
return PuzzleSubmission(
|
||||||
|
success=False,
|
||||||
|
flag=submission,
|
||||||
|
time=time,
|
||||||
|
hint_id=hint_id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if hint.pattern == submission:
|
||||||
|
return PuzzleSubmission(
|
||||||
|
success=False,
|
||||||
|
flag=submission,
|
||||||
|
time=time,
|
||||||
|
hint_id=hint_id,
|
||||||
|
)
|
||||||
|
return PuzzleSubmission(
|
||||||
|
success=False,
|
||||||
|
flag=submission,
|
||||||
|
time=time,
|
||||||
)
|
)
|
||||||
self.img_name = f"{img_id}{suffix}"
|
|
||||||
self.get_image_path().write_bytes(img)
|
|
||||||
|
|
||||||
def remove_image(self):
|
|
||||||
if self.img_name:
|
|
||||||
self.get_image_path().unlink(True)
|
|
||||||
self.img_name = ""
|
|
||||||
|
|
||||||
|
|
||||||
class PuzzleSubmission(BaseModel):
|
|
||||||
success: bool
|
|
||||||
flag: str
|
|
||||||
time: datetime.datetime
|
|
||||||
|
|
||||||
|
|
||||||
class DailyPuzzleInfo(BaseModel):
|
class DailyPuzzleInfo(BaseModel):
|
||||||
@ -86,15 +89,10 @@ class DailyPuzzleInfo(BaseModel):
|
|||||||
success_users: dict[str, datetime.datetime] = {}
|
success_users: dict[str, datetime.datetime] = {}
|
||||||
|
|
||||||
|
|
||||||
class PuzzleSubmissionResultMessage(BaseModel):
|
class PuzzleSubmissionFeedback(BaseModel):
|
||||||
success: bool
|
submission: PuzzleSubmission
|
||||||
rank: int = -1
|
puzzle: Puzzle
|
||||||
message: str = ""
|
info: DailyPuzzleInfo | None = None
|
||||||
|
|
||||||
def get_unimessage(self) -> UniMessage[Any]:
|
|
||||||
if self.success:
|
|
||||||
return UniMessage.text(f"🎉 恭喜你答对了!你是今天第 {self.rank} 个解出来的!")
|
|
||||||
return UniMessage.text(self.message)
|
|
||||||
|
|
||||||
|
|
||||||
def get_today_date() -> datetime.date:
|
def get_today_date() -> datetime.date:
|
||||||
@ -111,68 +109,61 @@ class PuzzleManager(BaseModel):
|
|||||||
daily_puzzle_of_date: dict[datetime.date, str] = {}
|
daily_puzzle_of_date: dict[datetime.date, str] = {}
|
||||||
|
|
||||||
puzzle_pinned: str = ""
|
puzzle_pinned: str = ""
|
||||||
unpublished_puzzles: set[str] = set()
|
|
||||||
unready_puzzles: set[str] = set()
|
|
||||||
published_puzzles: set[str] = set()
|
|
||||||
|
|
||||||
index_id_counter: int = 1
|
index_id_counter: int = 1
|
||||||
submissions: dict[str, dict[str, list[PuzzleSubmission]]] = {}
|
submissions: dict[str, dict[str, list[PuzzleSubmission]]] = {}
|
||||||
last_pubish_date: datetime.date = Field(
|
|
||||||
default_factory=lambda: get_today_date() - datetime.timedelta(days=1)
|
|
||||||
)
|
|
||||||
last_checked_date: datetime.date = Field(
|
last_checked_date: datetime.date = Field(
|
||||||
default_factory=lambda: get_today_date() - datetime.timedelta(days=1)
|
default_factory=lambda: get_today_date() - datetime.timedelta(days=1)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_publish_date(self):
|
||||||
|
return max(self.daily_puzzle_of_date.keys())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unpublished_puzzles(self):
|
||||||
|
return set((
|
||||||
|
p.raw_id for p in self.puzzle_data.values()
|
||||||
|
if not self.is_puzzle_published(p.raw_id) and p.ready
|
||||||
|
))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unready_puzzles(self):
|
||||||
|
return set((
|
||||||
|
p.raw_id for p in self.puzzle_data.values()
|
||||||
|
if not self.is_puzzle_published(p.raw_id) and not p.ready
|
||||||
|
))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def published_puzzles(self):
|
||||||
|
return set((
|
||||||
|
p.raw_id for p in self.puzzle_data.values()
|
||||||
|
if self.is_puzzle_published(p.raw_id)
|
||||||
|
))
|
||||||
|
|
||||||
|
def is_puzzle_published(self, raw_id: str):
|
||||||
|
return raw_id in [i.raw_id for i in self.daily_puzzle.values()]
|
||||||
|
|
||||||
def publish_puzzle(self, raw_id: str):
|
def publish_puzzle(self, raw_id: str):
|
||||||
assert raw_id in self.puzzle_data
|
assert raw_id in self.puzzle_data
|
||||||
|
|
||||||
self.unpublished_puzzles -= set(raw_id)
|
today = get_today_date()
|
||||||
self.unready_puzzles -= set(raw_id)
|
|
||||||
p = self.puzzle_data[raw_id]
|
p = self.puzzle_data[raw_id]
|
||||||
p.index_id = str(self.index_id_counter)
|
p.index_id = str(self.index_id_counter)
|
||||||
p.ready = True
|
p.ready = True
|
||||||
p.published = True
|
|
||||||
p.pinned = False
|
|
||||||
self.puzzle_pinned = ""
|
self.puzzle_pinned = ""
|
||||||
self.last_pubish_date = get_today_date()
|
self.last_checked_date = today
|
||||||
self.last_checked_date = self.last_pubish_date
|
|
||||||
self.daily_puzzle[p.index_id] = DailyPuzzleInfo(
|
self.daily_puzzle[p.index_id] = DailyPuzzleInfo(
|
||||||
raw_id=raw_id,
|
raw_id=raw_id,
|
||||||
time=self.last_pubish_date,
|
time=today,
|
||||||
)
|
)
|
||||||
self.daily_puzzle_of_date[self.last_pubish_date] = p.index_id
|
self.daily_puzzle_of_date[today] = p.index_id
|
||||||
self.published_puzzles.add(raw_id)
|
|
||||||
|
|
||||||
self.index_id_counter += 1
|
self.index_id_counter += 1
|
||||||
|
|
||||||
def admin_mark_ready(self, raw_id: str, ready: bool = True):
|
|
||||||
if raw_id not in self.puzzle_data:
|
|
||||||
return
|
|
||||||
if ready:
|
|
||||||
self.unready_puzzles -= set(raw_id)
|
|
||||||
if raw_id not in self.published_puzzles:
|
|
||||||
self.unpublished_puzzles.add(raw_id)
|
|
||||||
p = self.puzzle_data[raw_id]
|
|
||||||
p.ready = True
|
|
||||||
p.published = raw_id in self.published_puzzles
|
|
||||||
else:
|
|
||||||
self.unready_puzzles.add(raw_id)
|
|
||||||
self.unpublished_puzzles -= set(raw_id)
|
|
||||||
p = self.puzzle_data[raw_id]
|
|
||||||
p.ready = False
|
|
||||||
p.published = False
|
|
||||||
# if p.raw_id == self.puzzle_pinned:
|
|
||||||
# self.puzzle_pinned = ""
|
|
||||||
|
|
||||||
def admin_pin_puzzle(self, raw_id: str):
|
def admin_pin_puzzle(self, raw_id: str):
|
||||||
if self.puzzle_pinned:
|
|
||||||
p = self.puzzle_data.get(self.puzzle_pinned)
|
|
||||||
if p is not None:
|
|
||||||
p.pinned = False
|
|
||||||
if raw_id in self.puzzle_data:
|
if raw_id in self.puzzle_data:
|
||||||
p = self.puzzle_data[raw_id]
|
|
||||||
p.pinned = True
|
|
||||||
self.puzzle_pinned = raw_id
|
self.puzzle_pinned = raw_id
|
||||||
else:
|
else:
|
||||||
self.puzzle_pinned = ""
|
self.puzzle_pinned = ""
|
||||||
@ -202,41 +193,23 @@ class PuzzleManager(BaseModel):
|
|||||||
return
|
return
|
||||||
return self.daily_puzzle[p.index_id]
|
return self.daily_puzzle[p.index_id]
|
||||||
|
|
||||||
def submit(self, user: str, flag: str) -> PuzzleSubmissionResultMessage:
|
def submit(self, user: str, flag: str) -> PuzzleSubmissionFeedback | str:
|
||||||
p = self.get_today_puzzle()
|
p = self.get_today_puzzle()
|
||||||
d = self.get_today_info()
|
d = self.get_today_info()
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
if p is None or d is None:
|
if p is None or d is None:
|
||||||
return PuzzleSubmissionResultMessage(
|
return "今天没有题哦,改天再来吧!"
|
||||||
success=False,
|
|
||||||
message="今天没有题哦,改天再来吧!",
|
|
||||||
)
|
|
||||||
if user in d.success_users:
|
if user in d.success_users:
|
||||||
return PuzzleSubmissionResultMessage(
|
return "你今天已经答对过啦!不用重复提交哦!"
|
||||||
success=False,
|
|
||||||
message="你今天已经答对过啦!不用重复提交哦!",
|
|
||||||
)
|
|
||||||
if flag != p.flag:
|
|
||||||
d.tried_users.add(user)
|
|
||||||
self.submissions.setdefault(p.raw_id, {}).setdefault(user, []).append(PuzzleSubmission(
|
|
||||||
success=False,
|
|
||||||
flag=flag,
|
|
||||||
time=now,
|
|
||||||
))
|
|
||||||
return PuzzleSubmissionResultMessage(
|
|
||||||
success=False,
|
|
||||||
message="❌ 答错了,请检查你的答案哦",
|
|
||||||
)
|
|
||||||
d.tried_users.add(user)
|
d.tried_users.add(user)
|
||||||
d.success_users[user] = now
|
result = p.check_submission(flag, now)
|
||||||
self.submissions.setdefault(p.raw_id, {}).setdefault(user, []).append(PuzzleSubmission(
|
self.submissions.setdefault(p.raw_id, {}).setdefault(user, []).append(result)
|
||||||
success=True,
|
if result.success:
|
||||||
flag=flag,
|
d.success_users[user] = now
|
||||||
time=now,
|
return PuzzleSubmissionFeedback(
|
||||||
))
|
submission=result,
|
||||||
return PuzzleSubmissionResultMessage(
|
puzzle=p,
|
||||||
success=True,
|
info=d,
|
||||||
rank=len(d.success_users),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def admin_create_puzzle(self, user: str):
|
def admin_create_puzzle(self, user: str):
|
||||||
@ -252,9 +225,7 @@ class PuzzleManager(BaseModel):
|
|||||||
author_id=user,
|
author_id=user,
|
||||||
flag="konaph{this_is_a_flag}",
|
flag="konaph{this_is_a_flag}",
|
||||||
ready=False,
|
ready=False,
|
||||||
published=False,
|
|
||||||
)
|
)
|
||||||
self.unready_puzzles.add(p.raw_id)
|
|
||||||
self.puzzle_data[p.raw_id] = p
|
self.puzzle_data[p.raw_id] = p
|
||||||
return p
|
return p
|
||||||
|
|
||||||
@ -264,47 +235,20 @@ class PuzzleManager(BaseModel):
|
|||||||
if p.author_id == user
|
if p.author_id == user
|
||||||
], key=lambda p: p.created_at, reverse=True)
|
], key=lambda p: p.created_at, reverse=True)
|
||||||
|
|
||||||
def get_report_yesterday(self):
|
|
||||||
yesterday = get_today_date() - datetime.timedelta(days=1)
|
|
||||||
index_id = self.daily_puzzle_of_date.get(yesterday)
|
|
||||||
if index_id is None:
|
|
||||||
return None
|
|
||||||
info = self.daily_puzzle[index_id]
|
|
||||||
puzzle = self.puzzle_data[info.raw_id]
|
|
||||||
message = UniMessage.text(f"[KonaPH#{index_id}] 「{puzzle.title}」解答报告")
|
|
||||||
|
|
||||||
if len(info.success_users) == 0:
|
|
||||||
message = message.text(
|
|
||||||
"\n\n昨日,竟无人解出此题!"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
message = message.text(
|
|
||||||
f"\n\n昨日,共有 {len(info.success_users)} 人解出此题。\n\n前五名的解答者:"
|
|
||||||
)
|
|
||||||
us = [(u, d) for u, d in info.success_users.items()]
|
|
||||||
us = sorted(us, key=lambda t: t[1])
|
|
||||||
us = us[:5]
|
|
||||||
for u, _ in us:
|
|
||||||
m = self.submissions[puzzle.raw_id][u][-1]
|
|
||||||
message = message.text("- ").at(u).text(f" 于 {m.time.strftime('%H:%M')}")
|
|
||||||
|
|
||||||
message = message.text("\n\n出题者:").at(puzzle.author_id)
|
|
||||||
return message
|
|
||||||
|
|
||||||
|
|
||||||
lock = asyncio.Lock()
|
lock = asyncio.Lock()
|
||||||
|
|
||||||
|
|
||||||
def read_data():
|
def read_data():
|
||||||
try:
|
try:
|
||||||
data_raw = KONAPH_DATA_JSON.read_text()
|
data_raw = KONAPH_DATA_JSON.read_text("utf-8")
|
||||||
return PuzzleManager.model_validate_json(data_raw)
|
return PuzzleManager.model_validate_json(data_raw)
|
||||||
except (FileNotFoundError, ValidationError):
|
except (FileNotFoundError, ValidationError):
|
||||||
return PuzzleManager()
|
return PuzzleManager()
|
||||||
|
|
||||||
|
|
||||||
def write_data(data: PuzzleManager):
|
def write_data(data: PuzzleManager):
|
||||||
KONAPH_DATA_JSON.write_text(data.model_dump_json())
|
KONAPH_DATA_JSON.write_text(data.model_dump_json(), "utf-8")
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
|
|||||||
@ -1,14 +1,24 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from math import ceil
|
from math import ceil
|
||||||
from typing import Any
|
|
||||||
from nonebot import get_plugin_config
|
from nonebot import get_plugin_config
|
||||||
from nonebot_plugin_alconna import Alconna, Args, Image, Option, Query, Subcommand, UniMessage, on_alconna
|
from nonebot_plugin_alconna import (Alconna, Args, Image, Option, Query,
|
||||||
|
Subcommand, SubcommandResult, UniMessage,
|
||||||
|
on_alconna)
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from konabot.common.longtask import DepLongTaskTarget
|
from konabot.common.longtask import DepLongTaskTarget
|
||||||
|
from konabot.common.nb.exc import BotExceptionMessage
|
||||||
from konabot.common.nb.extract_image import download_image_bytes
|
from konabot.common.nb.extract_image import download_image_bytes
|
||||||
from konabot.common.nb.qq_broadcast import qq_broadcast
|
from konabot.common.nb.qq_broadcast import qq_broadcast
|
||||||
from konabot.plugins.kona_ph.core.storage import Puzzle, get_today_date, puzzle_manager
|
from konabot.common.username import get_username
|
||||||
|
from konabot.plugins.kona_ph.core.image import get_image_manager
|
||||||
|
from konabot.plugins.kona_ph.core.message import (get_puzzle_description, get_puzzle_hint_list,
|
||||||
|
get_puzzle_info_message,
|
||||||
|
get_submission_message)
|
||||||
|
from konabot.plugins.kona_ph.core.storage import (Puzzle, PuzzleHint, PuzzleManager,
|
||||||
|
get_today_date,
|
||||||
|
puzzle_manager)
|
||||||
|
|
||||||
PUZZLE_PAGE_SIZE = 10
|
PUZZLE_PAGE_SIZE = 10
|
||||||
|
|
||||||
@ -30,31 +40,15 @@ def is_puzzle_admin(target: DepLongTaskTarget):
|
|||||||
return target.target_id in config.plugin_puzzle_admin
|
return target.target_id in config.plugin_puzzle_admin
|
||||||
|
|
||||||
|
|
||||||
def get_puzzle_info_message(puzzle: Puzzle) -> UniMessage[Any]:
|
def check_puzzle(manager: PuzzleManager, target: DepLongTaskTarget, raw_id: str) -> Puzzle:
|
||||||
status = "✅ 已准备,待发布" if puzzle.ready and not puzzle.published else \
|
if raw_id not in manager.puzzle_data:
|
||||||
(f"🟢 已发布: #{puzzle.index_id}" if puzzle.published else "⚙️ 未准备")
|
raise BotExceptionMessage("没有这个谜题")
|
||||||
|
puzzle = manager.puzzle_data[raw_id]
|
||||||
status_suffix = ""
|
if is_puzzle_admin(target):
|
||||||
if puzzle.pinned:
|
return puzzle
|
||||||
status_suffix += " | 📌 已被管理员置顶"
|
if target.target_id != puzzle.author_id:
|
||||||
|
raise BotExceptionMessage("你没有权限查看或编辑这个谜题")
|
||||||
msg = UniMessage.text(
|
return puzzle
|
||||||
f"--- 谜题信息 ---\n"
|
|
||||||
f"Raw ID: {puzzle.raw_id}\n"
|
|
||||||
f"标题: {puzzle.title}\n"
|
|
||||||
f"出题者 ID: {puzzle.author_id}\n"
|
|
||||||
f"创建时间: {puzzle.created_at.strftime('%Y-%m-%d %H:%M:%S')}\n"
|
|
||||||
f"Flag: {puzzle.flag}\n"
|
|
||||||
f"状态: {status}{status_suffix}\n\n"
|
|
||||||
f"{puzzle.content}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if puzzle.img_name:
|
|
||||||
msg = msg.image(raw=puzzle.get_image_path().read_bytes())
|
|
||||||
|
|
||||||
msg = msg.text(f"\n---------\n使用 `konaph ready {puzzle.raw_id}` 完成编辑")
|
|
||||||
|
|
||||||
return msg
|
|
||||||
|
|
||||||
|
|
||||||
def create_admin_commands():
|
def create_admin_commands():
|
||||||
@ -80,6 +74,46 @@ def create_admin_commands():
|
|||||||
dest="modify",
|
dest="modify",
|
||||||
),
|
),
|
||||||
Subcommand("publish", Args["raw_id?", str], dest="publish"),
|
Subcommand("publish", Args["raw_id?", str], dest="publish"),
|
||||||
|
Subcommand("preview", Args["raw_id", str], dest="preview"),
|
||||||
|
Subcommand("get-submits", Args["raw_id", str], dest="get-submits"),
|
||||||
|
Subcommand(
|
||||||
|
"test",
|
||||||
|
Args["raw_id", str],
|
||||||
|
Args["submission", str],
|
||||||
|
dest="test",
|
||||||
|
),
|
||||||
|
Subcommand(
|
||||||
|
"hint",
|
||||||
|
Subcommand(
|
||||||
|
"add",
|
||||||
|
Args["raw_id", str],
|
||||||
|
Args["pattern", str],
|
||||||
|
Args["message", str],
|
||||||
|
dest="add",
|
||||||
|
),
|
||||||
|
Subcommand(
|
||||||
|
"list",
|
||||||
|
Args["raw_id", str],
|
||||||
|
Args["page?", int],
|
||||||
|
dest="list",
|
||||||
|
),
|
||||||
|
Subcommand(
|
||||||
|
"modify",
|
||||||
|
Args["raw_id", str],
|
||||||
|
Args["hint_id", int],
|
||||||
|
Option("--pattern", Args["pattern", str], alias=["-p"]),
|
||||||
|
Option("--message", Args["message", str], alias=["-m"]),
|
||||||
|
Option("--checkpoint", Args["is_checkpoint", bool], alias=["-c"]),
|
||||||
|
dest="modify",
|
||||||
|
),
|
||||||
|
Subcommand(
|
||||||
|
"delete",
|
||||||
|
Args["raw_id", str],
|
||||||
|
Args["hint_id", int],
|
||||||
|
dest="delete",
|
||||||
|
),
|
||||||
|
dest="hint",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
rule=is_puzzle_manager,
|
rule=is_puzzle_manager,
|
||||||
)
|
)
|
||||||
@ -93,6 +127,10 @@ def create_admin_commands():
|
|||||||
msg = msg.text("konaph info <id> - 查看谜题\n")
|
msg = msg.text("konaph info <id> - 查看谜题\n")
|
||||||
msg = msg.text("konaph my <page?> - 查看我的谜题列表\n")
|
msg = msg.text("konaph my <page?> - 查看我的谜题列表\n")
|
||||||
msg = msg.text("konaph modify - 查看如何修改谜题信息\n")
|
msg = msg.text("konaph modify - 查看如何修改谜题信息\n")
|
||||||
|
msg = msg.text("konaph preview <id> - 预览一个题目的效果,不会展示答案\n")
|
||||||
|
msg = msg.text("konaph get-submits <id> - 获得题目的提交记录\n")
|
||||||
|
msg = msg.text("konaph test <id> <answer> - 尝试提交一个答案,看回答的效果\n")
|
||||||
|
msg = msg.text("konaph hint - 查看如何编辑题目的中间答案\n")
|
||||||
|
|
||||||
if is_puzzle_admin(target):
|
if is_puzzle_admin(target):
|
||||||
msg = msg.text("konaph all [--ready] <page?> - 查看所有谜题\n")
|
msg = msg.text("konaph all [--ready] <page?> - 查看所有谜题\n")
|
||||||
@ -117,20 +155,12 @@ def create_admin_commands():
|
|||||||
@cmd_admin.assign("ready")
|
@cmd_admin.assign("ready")
|
||||||
async def _(raw_id: str, target: DepLongTaskTarget):
|
async def _(raw_id: str, target: DepLongTaskTarget):
|
||||||
async with puzzle_manager() as manager:
|
async with puzzle_manager() as manager:
|
||||||
if raw_id not in manager.puzzle_data:
|
p = check_puzzle(manager, target, raw_id)
|
||||||
return await target.send_message(UniMessage.text(
|
|
||||||
"你输入的谜题不存在!输入 `konaph my` 查看你创建的谜题"
|
|
||||||
))
|
|
||||||
p = manager.puzzle_data[raw_id]
|
|
||||||
if p.author_id != target.target_id and not is_puzzle_admin(target):
|
|
||||||
return await target.send_message(UniMessage.text(
|
|
||||||
"这不是你的题,你没有权限编辑!输入 `konaph my` 查看你创建的谜题"
|
|
||||||
))
|
|
||||||
if p.ready:
|
if p.ready:
|
||||||
return await target.send_message(UniMessage.text(
|
return await target.send_message(UniMessage.text(
|
||||||
"题目早就准备好啦!"
|
"题目早就准备好啦!"
|
||||||
))
|
))
|
||||||
manager.admin_mark_ready(raw_id, True)
|
p.ready = True
|
||||||
await target.send_message(UniMessage.text(
|
await target.send_message(UniMessage.text(
|
||||||
f"谜题「{p.title}」已经准备就绪!"
|
f"谜题「{p.title}」已经准备就绪!"
|
||||||
))
|
))
|
||||||
@ -138,25 +168,17 @@ def create_admin_commands():
|
|||||||
@cmd_admin.assign("unready")
|
@cmd_admin.assign("unready")
|
||||||
async def _(raw_id: str, target: DepLongTaskTarget):
|
async def _(raw_id: str, target: DepLongTaskTarget):
|
||||||
async with puzzle_manager() as manager:
|
async with puzzle_manager() as manager:
|
||||||
if raw_id not in manager.puzzle_data:
|
p = check_puzzle(manager, target, raw_id)
|
||||||
return await target.send_message(UniMessage.text(
|
|
||||||
"你输入的谜题不存在!输入 `konaph my` 查看你创建的谜题"
|
|
||||||
))
|
|
||||||
p = manager.puzzle_data[raw_id]
|
|
||||||
if p.author_id != target.target_id and not is_puzzle_admin(target):
|
|
||||||
return await target.send_message(UniMessage.text(
|
|
||||||
"这不是你的题,你没有权限编辑!输入 `konaph my` 查看你创建的谜题"
|
|
||||||
))
|
|
||||||
if not p.ready:
|
if not p.ready:
|
||||||
return await target.send_message(UniMessage.text(
|
return await target.send_message(UniMessage.text(
|
||||||
f"谜题「{p.title}」已经是未取消状态了!"
|
f"谜题「{p.title}」已经是未取消状态了!"
|
||||||
))
|
))
|
||||||
if p.published:
|
if manager.is_puzzle_published(p.raw_id):
|
||||||
return await target.send_message(UniMessage.text(
|
return await target.send_message(UniMessage.text(
|
||||||
"已发布的谜题不能取消准备状态!"
|
"已发布的谜题不能取消准备状态!"
|
||||||
))
|
))
|
||||||
|
|
||||||
manager.admin_mark_ready(raw_id, False)
|
p.ready = False
|
||||||
await target.send_message(UniMessage.text(
|
await target.send_message(UniMessage.text(
|
||||||
f"谜题「{p.title}」已经取消准备!"
|
f"谜题「{p.title}」已经取消准备!"
|
||||||
))
|
))
|
||||||
@ -164,17 +186,8 @@ def create_admin_commands():
|
|||||||
@cmd_admin.assign("info")
|
@cmd_admin.assign("info")
|
||||||
async def _(raw_id: str, target: DepLongTaskTarget):
|
async def _(raw_id: str, target: DepLongTaskTarget):
|
||||||
async with puzzle_manager() as manager:
|
async with puzzle_manager() as manager:
|
||||||
if raw_id not in manager.puzzle_data:
|
p = check_puzzle(manager, target, raw_id)
|
||||||
return await target.send_message(UniMessage.text(
|
await target.send_message(get_puzzle_info_message(manager, p))
|
||||||
"你输入的谜题不存在!输入 `konaph my` 查看你创建的谜题"
|
|
||||||
))
|
|
||||||
p = manager.puzzle_data[raw_id]
|
|
||||||
if p.author_id != target.target_id and not is_puzzle_admin(target):
|
|
||||||
return await target.send_message(UniMessage.text(
|
|
||||||
"这不是你的题,你没有权限查看详细信息!"
|
|
||||||
))
|
|
||||||
|
|
||||||
await target.send_message(get_puzzle_info_message(p))
|
|
||||||
|
|
||||||
@cmd_admin.assign("my")
|
@cmd_admin.assign("my")
|
||||||
async def _(target: DepLongTaskTarget, page: int = 1):
|
async def _(target: DepLongTaskTarget, page: int = 1):
|
||||||
@ -193,10 +206,10 @@ def create_admin_commands():
|
|||||||
message = UniMessage.text("==== 我的谜题 ====\n\n")
|
message = UniMessage.text("==== 我的谜题 ====\n\n")
|
||||||
for p in puzzles:
|
for p in puzzles:
|
||||||
message = message.text("- ")
|
message = message.text("- ")
|
||||||
if p.pinned:
|
if manager.puzzle_pinned == p.raw_id:
|
||||||
message = message.text("[📌]")
|
message = message.text("[📌]")
|
||||||
if p.published:
|
if manager.is_puzzle_published(p.raw_id):
|
||||||
message = message.text(f"[#{p.index_id}] ")
|
message = message.text(f"[✨][#{p.index_id}] ")
|
||||||
elif p.ready:
|
elif p.ready:
|
||||||
message = message.text("[✅] ")
|
message = message.text("[✅] ")
|
||||||
else:
|
else:
|
||||||
@ -207,9 +220,11 @@ def create_admin_commands():
|
|||||||
await target.send_message(message)
|
await target.send_message(message)
|
||||||
|
|
||||||
@cmd_admin.assign("all")
|
@cmd_admin.assign("all")
|
||||||
async def _(target: DepLongTaskTarget, ready: Query[bool] = Query("ready"), page: int = 1):
|
async def _(target: DepLongTaskTarget, ready: Query[bool] = Query("all.ready"), page: int = 1):
|
||||||
if not is_puzzle_admin(target):
|
if not is_puzzle_admin(target):
|
||||||
return await target.send_message(UniMessage.text("你没有权限查看所有的哦"))
|
return await target.send_message(UniMessage.text(
|
||||||
|
"你没有权限使用该指令"
|
||||||
|
))
|
||||||
async with puzzle_manager() as manager:
|
async with puzzle_manager() as manager:
|
||||||
puzzles = [*manager.puzzle_data.values()]
|
puzzles = [*manager.puzzle_data.values()]
|
||||||
if ready.available:
|
if ready.available:
|
||||||
@ -224,10 +239,10 @@ def create_admin_commands():
|
|||||||
message = UniMessage.text("==== 所有谜题 ====\n\n")
|
message = UniMessage.text("==== 所有谜题 ====\n\n")
|
||||||
for p in puzzles:
|
for p in puzzles:
|
||||||
message = message.text("- ")
|
message = message.text("- ")
|
||||||
if p.pinned:
|
if p.raw_id == manager.puzzle_pinned:
|
||||||
message = message.text("[📌]")
|
message = message.text("[📌]")
|
||||||
if p.published:
|
if manager.is_puzzle_published(p.raw_id):
|
||||||
message = message.text(f"[#{p.index_id}] ")
|
message = message.text(f"[✨][#{p.index_id}] ")
|
||||||
elif p.ready:
|
elif p.ready:
|
||||||
message = message.text("[✅] ")
|
message = message.text("[✅] ")
|
||||||
else:
|
else:
|
||||||
@ -276,7 +291,7 @@ def create_admin_commands():
|
|||||||
description: str | None = None,
|
description: str | None = None,
|
||||||
flag: str | None = None,
|
flag: str | None = None,
|
||||||
image: Image | None = None,
|
image: Image | None = None,
|
||||||
remove_image: Query[bool] = Query("--remove-image"),
|
remove_image: Query[bool] = Query("modify.remove-image"),
|
||||||
):
|
):
|
||||||
if raw_id == "":
|
if raw_id == "":
|
||||||
return await target.send_message(
|
return await target.send_message(
|
||||||
@ -284,35 +299,41 @@ def create_admin_commands():
|
|||||||
"支持的参数:\n"
|
"支持的参数:\n"
|
||||||
" --title <str> 标题\n"
|
" --title <str> 标题\n"
|
||||||
" --description <str> 题目详情描述(用直引号包裹以支持多行)\n"
|
" --description <str> 题目详情描述(用直引号包裹以支持多行)\n"
|
||||||
" --flag <str> flag\n"
|
" --flag <str> flag,也就是题目的答案\n"
|
||||||
" --image <图片> 图片\n"
|
" --image <图片> 图片\n"
|
||||||
" --remove-image 删除图片"
|
" --remove-image 删除图片"
|
||||||
)
|
)
|
||||||
|
image_manager = get_image_manager()
|
||||||
|
|
||||||
async with puzzle_manager() as manager:
|
async with puzzle_manager() as manager:
|
||||||
if raw_id not in manager.puzzle_data:
|
p = check_puzzle(manager, target, raw_id)
|
||||||
return await target.send_message("没有这个谜题")
|
|
||||||
p = manager.puzzle_data[raw_id]
|
|
||||||
if not is_puzzle_admin(target) and target.target_id != p.author_id:
|
|
||||||
return await target.send_message("你没有权限编辑这个谜题")
|
|
||||||
if title is not None:
|
if title is not None:
|
||||||
p.title = title
|
p.title = title
|
||||||
if description is not None:
|
if description is not None:
|
||||||
p.content = description
|
p.content = description
|
||||||
if flag is not None:
|
if flag is not None:
|
||||||
p.flag = flag
|
p.flag = flag.strip()
|
||||||
|
if flag.strip() != flag:
|
||||||
|
await target.send_message(
|
||||||
|
"⚠️ 注意:你输入的 Flag 含有开头或结尾的空格,已经帮你去除"
|
||||||
|
)
|
||||||
if image is not None and image.url is not None:
|
if image is not None and image.url is not None:
|
||||||
b = await download_image_bytes(image.url)
|
b = await download_image_bytes(image.url)
|
||||||
p.add_image(b.unwrap())
|
image_manager.remove_puzzle_image(p.img_name)
|
||||||
|
p.img_name = image_manager.upload_puzzle_image(b.unwrap())
|
||||||
elif remove_image.available:
|
elif remove_image.available:
|
||||||
p.remove_image()
|
image_manager.remove_puzzle_image(p.img_name)
|
||||||
|
|
||||||
info2 = get_puzzle_info_message(p)
|
info2 = get_puzzle_info_message(manager, p)
|
||||||
|
|
||||||
return await target.send_message("修改好啦!看看效果:\n\n" + info2)
|
return await target.send_message("修改好啦!看看效果:\n\n" + info2)
|
||||||
|
|
||||||
@cmd_admin.assign("publish")
|
@cmd_admin.assign("publish")
|
||||||
async def _(target: DepLongTaskTarget, raw_id: str | None = None):
|
async def _(target: DepLongTaskTarget, raw_id: str | None = None):
|
||||||
|
if not is_puzzle_admin(target):
|
||||||
|
return await target.send_message(UniMessage.text(
|
||||||
|
"你没有权限使用该指令"
|
||||||
|
))
|
||||||
today = get_today_date()
|
today = get_today_date()
|
||||||
async with puzzle_manager() as manager:
|
async with puzzle_manager() as manager:
|
||||||
if today in manager.daily_puzzle_of_date:
|
if today in manager.daily_puzzle_of_date:
|
||||||
@ -323,7 +344,121 @@ def create_admin_commands():
|
|||||||
p = manager.get_today_puzzle(strong=True)
|
p = manager.get_today_puzzle(strong=True)
|
||||||
if p is None:
|
if p is None:
|
||||||
return await target.send_message("上架失败了orz,可能是没题了")
|
return await target.send_message("上架失败了orz,可能是没题了")
|
||||||
await qq_broadcast(config.plugin_puzzle_playgroup, p.get_unimessage())
|
await qq_broadcast(config.plugin_puzzle_playgroup, get_puzzle_description(p))
|
||||||
return await target.send_message("Ok!")
|
return await target.send_message("Ok!")
|
||||||
|
|
||||||
|
@cmd_admin.assign("preview")
|
||||||
|
async def _(target: DepLongTaskTarget, raw_id: str):
|
||||||
|
async with puzzle_manager() as manager:
|
||||||
|
p = check_puzzle(manager, target, raw_id)
|
||||||
|
return await target.send_message(get_puzzle_description(p))
|
||||||
|
|
||||||
|
@cmd_admin.assign("get-submits")
|
||||||
|
async def _(target: DepLongTaskTarget, raw_id: str):
|
||||||
|
async with puzzle_manager() as manager:
|
||||||
|
puzzle = manager.puzzle_data.get(raw_id)
|
||||||
|
if puzzle is None:
|
||||||
|
return await target.send_message("没有这个谜题")
|
||||||
|
if not is_puzzle_admin(target) and target.target_id != puzzle.author_id:
|
||||||
|
return await target.send_message("你没有权限预览这个谜题")
|
||||||
|
|
||||||
|
msg = UniMessage.text(f"==== {puzzle.title} 提交记录 ====\n\n")
|
||||||
|
submits = manager.submissions.get(raw_id, {})
|
||||||
|
for uid, ls in submits.items():
|
||||||
|
s = ', '.join((i.flag for i in ls))
|
||||||
|
msg = msg.text(f"- {get_username(uid)}:{s}\n")
|
||||||
|
return await target.send_message(msg)
|
||||||
|
|
||||||
|
@cmd_admin.assign("test")
|
||||||
|
async def _(target: DepLongTaskTarget, raw_id: str, submission: str):
|
||||||
|
"""
|
||||||
|
测试一道谜题的回答,并给出结果
|
||||||
|
"""
|
||||||
|
async with puzzle_manager() as manager:
|
||||||
|
p = check_puzzle(manager, target, raw_id)
|
||||||
|
result = p.check_submission(submission)
|
||||||
|
msg = get_submission_message(p, result)
|
||||||
|
return await target.send_message("[测试提交] " + msg)
|
||||||
|
|
||||||
|
@cmd_admin.assign("subcommands.hint")
|
||||||
|
async def _(target: DepLongTaskTarget, subcommands: Query[SubcommandResult] = Query("subcommands.hint")):
|
||||||
|
if len(subcommands.result.subcommands) > 0:
|
||||||
|
return
|
||||||
|
return await target.send_message(
|
||||||
|
UniMessage.text("==== 提示/中间答案编辑器 ====\n\n")
|
||||||
|
.text("- konaph hint list <id>\n - 查看某道题的所有提示 / 中间答案\n")
|
||||||
|
.text("- konaph hint add <id> <pattern> <hint>\n - 添加一个提示 / 中间答案\n")
|
||||||
|
.text("- konaph hint modify <id> <hint_id>\n")
|
||||||
|
.text(" - --pattern <pattern>\n - 更改匹配规则\n")
|
||||||
|
.text(" - --message <message>\n - 更改提示文本\n")
|
||||||
|
.text(" - --checkpoint [True|False]\n - 更改是否为中间答案\n")
|
||||||
|
.text("- konaph hint delete <id> <hint_id>\n - 删除一个提示 / 中间答案\n")
|
||||||
|
.text("\n更多关于 pattern 和中间答案的信息,请见 man:中间答案(7)")
|
||||||
|
)
|
||||||
|
|
||||||
|
@cmd_admin.assign("subcommands.hint.add")
|
||||||
|
async def _(
|
||||||
|
target: DepLongTaskTarget,
|
||||||
|
raw_id: str,
|
||||||
|
pattern: str,
|
||||||
|
message: str,
|
||||||
|
):
|
||||||
|
async with puzzle_manager() as manager:
|
||||||
|
p = check_puzzle(manager, target, raw_id)
|
||||||
|
p.hints[p.hint_id_max + 1] = PuzzleHint(
|
||||||
|
pattern=pattern,
|
||||||
|
message=message,
|
||||||
|
is_checkpoint=False,
|
||||||
|
)
|
||||||
|
await target.send_message("创建成功!\n\n" + get_puzzle_hint_list(p))
|
||||||
|
|
||||||
|
@cmd_admin.assign("subcommands.hint.list")
|
||||||
|
async def _(
|
||||||
|
target: DepLongTaskTarget,
|
||||||
|
raw_id: str,
|
||||||
|
):
|
||||||
|
async with puzzle_manager() as manager:
|
||||||
|
p = check_puzzle(manager, target, raw_id)
|
||||||
|
await target.send_message(get_puzzle_hint_list(p))
|
||||||
|
|
||||||
|
@cmd_admin.assign("subcommands.hint.modify")
|
||||||
|
async def _(
|
||||||
|
target: DepLongTaskTarget,
|
||||||
|
raw_id: str,
|
||||||
|
hint_id: int,
|
||||||
|
pattern: str | None = None,
|
||||||
|
message: str | None = None,
|
||||||
|
is_checkpoint: bool | None = None,
|
||||||
|
):
|
||||||
|
async with puzzle_manager() as manager:
|
||||||
|
p = check_puzzle(manager, target, raw_id)
|
||||||
|
if hint_id not in p.hints:
|
||||||
|
raise BotExceptionMessage(
|
||||||
|
f"没有这个 hint_id。请使用 konaph hint list {raw_id} 了解 hint 清单"
|
||||||
|
)
|
||||||
|
hint = p.hints[hint_id]
|
||||||
|
if pattern is not None:
|
||||||
|
hint.pattern = pattern
|
||||||
|
if message is not None:
|
||||||
|
hint.message = message
|
||||||
|
if is_checkpoint is not None:
|
||||||
|
hint.is_checkpoint = is_checkpoint
|
||||||
|
await target.send_message("更改成功!\n\n" + get_puzzle_hint_list(p))
|
||||||
|
|
||||||
|
@cmd_admin.assign("subcommands.hint.delete")
|
||||||
|
async def _(
|
||||||
|
target: DepLongTaskTarget,
|
||||||
|
raw_id: str,
|
||||||
|
hint_id: int,
|
||||||
|
):
|
||||||
|
async with puzzle_manager() as manager:
|
||||||
|
p = check_puzzle(manager, target, raw_id)
|
||||||
|
if hint_id not in p.hints:
|
||||||
|
raise BotExceptionMessage(
|
||||||
|
f"没有这个 hint_id。请使用 konaph hint list {raw_id} 了解 hint 清单"
|
||||||
|
)
|
||||||
|
del p.hints[hint_id]
|
||||||
|
await target.send_message("删除成功!\n\n" + get_puzzle_hint_list(p))
|
||||||
|
|
||||||
|
|
||||||
return cmd_admin
|
return cmd_admin
|
||||||
|
|||||||
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,4 +1,3 @@
|
|||||||
from curses.ascii import isdigit
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import nonebot
|
import nonebot
|
||||||
@ -40,7 +39,10 @@ async def _(
|
|||||||
doc: str | None,
|
doc: str | None,
|
||||||
event: nonebot.adapters.Event,
|
event: nonebot.adapters.Event,
|
||||||
):
|
):
|
||||||
if doc is not None and section is None and all(isdigit(c) for c in doc):
|
if doc is not None and section is None and all(
|
||||||
|
ord('0') <= ord(c) <= ord('9')
|
||||||
|
for c in doc
|
||||||
|
):
|
||||||
section = int(doc)
|
section = int(doc)
|
||||||
doc = None
|
doc = None
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ if not POLL_DATA_FILE.exists():
|
|||||||
POLL_DATA_FILE.write_bytes(POLL_TEMPLATE_FILE.read_bytes())
|
POLL_DATA_FILE.write_bytes(POLL_TEMPLATE_FILE.read_bytes())
|
||||||
|
|
||||||
|
|
||||||
poll_list = json.loads(POLL_DATA_FILE.read_text())['poll']
|
poll_list = json.loads(POLL_DATA_FILE.read_text("utf-8"))['poll']
|
||||||
|
|
||||||
async def createpoll(title,qqid,options):
|
async def createpoll(title,qqid,options):
|
||||||
polllength = len(poll_list)
|
polllength = len(poll_list)
|
||||||
@ -53,7 +53,7 @@ def writeback():
|
|||||||
# json.dump({'poll':poll_list},file,ensure_ascii=False,sort_keys=True)
|
# json.dump({'poll':poll_list},file,ensure_ascii=False,sort_keys=True)
|
||||||
POLL_DATA_FILE.write_text(json.dumps({
|
POLL_DATA_FILE.write_text(json.dumps({
|
||||||
'poll': poll_list,
|
'poll': poll_list,
|
||||||
}, ensure_ascii=False, sort_keys=True))
|
}, ensure_ascii=False, sort_keys=True), "utf-8")
|
||||||
|
|
||||||
async def pollvote(polnum,optionnum,qqnum):
|
async def pollvote(polnum,optionnum,qqnum):
|
||||||
optiond = poll_list[polnum]["polldata"]
|
optiond = poll_list[polnum]["polldata"]
|
||||||
|
|||||||
@ -394,7 +394,8 @@ async def generate_dice_image(number: str) -> BytesIO:
|
|||||||
append_images=images[1:],
|
append_images=images[1:],
|
||||||
duration=frame_durations,
|
duration=frame_durations,
|
||||||
format='GIF',
|
format='GIF',
|
||||||
loop=1)
|
loop=1,
|
||||||
|
disposal=2)
|
||||||
output.seek(0)
|
output.seek(0)
|
||||||
# pil_final.save(output, format='PNG')
|
# pil_final.save(output, format='PNG')
|
||||||
return output
|
return output
|
||||||
@ -4,10 +4,13 @@ from typing import cast
|
|||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from nonebot import get_bot, on_request
|
from nonebot import get_bot, on_request
|
||||||
|
import nonebot
|
||||||
from nonebot.adapters.onebot.v11.event import FriendRequestEvent
|
from nonebot.adapters.onebot.v11.event import FriendRequestEvent
|
||||||
from nonebot.adapters.onebot.v11.bot import Bot as OnebotBot
|
from nonebot.adapters.onebot.v11.bot import Bot as OnebotBot
|
||||||
|
from nonebot_plugin_apscheduler import scheduler
|
||||||
|
|
||||||
from konabot.common.nb.is_admin import cfg as adminConfig
|
from konabot.common.nb.is_admin import cfg as adminConfig
|
||||||
|
from konabot.common.username import manager
|
||||||
|
|
||||||
add_request = on_request()
|
add_request = on_request()
|
||||||
|
|
||||||
@ -23,3 +26,15 @@ async def _(req: FriendRequestEvent):
|
|||||||
await req.approve(bot)
|
await req.approve(bot)
|
||||||
logger.info(f"已经自动同意 {req.user_id} 的好友请求")
|
logger.info(f"已经自动同意 {req.user_id} 的好友请求")
|
||||||
|
|
||||||
|
@scheduler.scheduled_job("cron", minute="*/5")
|
||||||
|
async def _():
|
||||||
|
logger.info("尝试更新群成员信息")
|
||||||
|
await manager.update()
|
||||||
|
|
||||||
|
driver = nonebot.get_driver()
|
||||||
|
|
||||||
|
@driver.on_bot_connect
|
||||||
|
async def _():
|
||||||
|
logger.info("有 Bot 连接,5 秒后试着更新群成员信息")
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
await manager.update()
|
||||||
@ -59,14 +59,14 @@ def load_notify_config() -> NotifyConfigFile:
|
|||||||
if not DATA_FILE_PATH.exists():
|
if not DATA_FILE_PATH.exists():
|
||||||
return NotifyConfigFile()
|
return NotifyConfigFile()
|
||||||
try:
|
try:
|
||||||
return NotifyConfigFile.model_validate_json(DATA_FILE_PATH.read_text())
|
return NotifyConfigFile.model_validate_json(DATA_FILE_PATH.read_text("utf-8"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"在解析 Notify 时遇到问题:{e}")
|
logger.warning(f"在解析 Notify 时遇到问题:{e}")
|
||||||
return NotifyConfigFile()
|
return NotifyConfigFile()
|
||||||
|
|
||||||
|
|
||||||
def save_notify_config(config: NotifyConfigFile):
|
def save_notify_config(config: NotifyConfigFile):
|
||||||
DATA_FILE_PATH.write_text(config.model_dump_json(indent=4))
|
DATA_FILE_PATH.write_text(config.model_dump_json(indent=4), "utf-8")
|
||||||
|
|
||||||
|
|
||||||
@evt.handle()
|
@evt.handle()
|
||||||
|
|||||||
196
poetry.lock
generated
196
poetry.lock
generated
@ -803,6 +803,23 @@ type = "legacy"
|
|||||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||||
reference = "mirrors"
|
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]]
|
[[package]]
|
||||||
name = "exceptiongroup"
|
name = "exceptiongroup"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
@ -1333,6 +1350,123 @@ type = "legacy"
|
|||||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||||
reference = "mirrors"
|
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]]
|
[[package]]
|
||||||
name = "linkify-it-py"
|
name = "linkify-it-py"
|
||||||
version = "2.0.3"
|
version = "2.0.3"
|
||||||
@ -2200,6 +2334,39 @@ type = "legacy"
|
|||||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||||
reference = "mirrors"
|
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]]
|
[[package]]
|
||||||
name = "opencc"
|
name = "opencc"
|
||||||
version = "1.1.9"
|
version = "1.1.9"
|
||||||
@ -3387,6 +3554,33 @@ type = "legacy"
|
|||||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||||
reference = "mirrors"
|
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]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.15.0"
|
version = "4.15.0"
|
||||||
@ -3978,4 +4172,4 @@ reference = "mirrors"
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.12,<4.0"
|
python-versions = ">=3.12,<4.0"
|
||||||
content-hash = "ec73430f70658a303c47e6f536ccb0863a475f7f25d5334c8766e6149075648c"
|
content-hash = "dcb6567ccb9eb6357179dd8b8eaa5fb69373cef0e17d3a49c7c895d289c0d642"
|
||||||
|
|||||||
@ -27,6 +27,7 @@ dependencies = [
|
|||||||
"nanoid (>=2.0.0,<3.0.0)",
|
"nanoid (>=2.0.0,<3.0.0)",
|
||||||
"opencc (>=1.1.9,<2.0.0)",
|
"opencc (>=1.1.9,<2.0.0)",
|
||||||
"playwright (>=1.55.0,<2.0.0)",
|
"playwright (>=1.55.0,<2.0.0)",
|
||||||
|
"openai (>=2.7.1,<3.0.0)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|||||||
13
scripts/watch_filter.py
Normal file
13
scripts/watch_filter.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from watchfiles import Change
|
||||||
|
|
||||||
|
|
||||||
|
base = Path(__file__).parent.parent.absolute()
|
||||||
|
|
||||||
|
|
||||||
|
def filter(change: Change, path: str) -> bool:
|
||||||
|
if "__pycache__" in path:
|
||||||
|
return False
|
||||||
|
if Path(path).absolute().is_relative_to(base / "data"):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
Reference in New Issue
Block a user