Compare commits

..

14 Commits

Author SHA1 Message Date
40be5ce335 fx 完善
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-02 14:45:07 +08:00
8e6131473d fximage 2025-12-02 12:17:11 +08:00
78bda5fc0a 添加云盾
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-28 16:59:58 +08:00
97658a6c56 补充文档
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-28 16:54:29 +08:00
3fedc685a9 没有人需要的提取首字母功能 2025-11-28 16:51:16 +08:00
d1a3e44c45 调整日志等级和内容
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-26 13:09:37 +08:00
f637778173 完成排行榜部分
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-26 13:02:26 +08:00
145bfedf67 Merge branch 'master' of ssh://gitea.service.jazzwhom.top:2221/mttu-developers/konabot
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-25 14:29:46 +08:00
61b9d733a5 添加阿里绿网云盾 API 2025-11-25 14:29:26 +08:00
ae59c20e2f 添加对 pyrightconfig 的 ignore,方便使用其他 IDE 的人使用 config 文件指定虚拟环境位置等
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-24 18:36:06 +08:00
0b7d21aeb0 新增一条示例,以便处理几百年的(
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-22 01:22:42 +08:00
d6ede3e6cd 标准化时间的解析
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-21 16:56:00 +08:00
07ace8e6e9 就是加个 Y 的事情(
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-21 16:19:19 +08:00
6f08c22b5b LLM 胜利了!!!!!!
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-21 16:13:38 +08:00
25 changed files with 1086 additions and 66 deletions

6
.gitignore vendored
View File

@ -1,5 +1,11 @@
# 基本的数据文件,以及环境用文件
/.env
/data
/pyrightconfig.json
/pyrightconfig.toml
# 缓存文件
__pycache__
# 可能会偶然生成的 diff 文件
/*.diff

View File

@ -0,0 +1,90 @@
import asyncio
import json
from alibabacloud_green20220302.client import Client as AlibabaGreenClient
from alibabacloud_green20220302.models import TextModerationPlusRequest
from alibabacloud_tea_openapi.models import Config as AlibabaTeaConfig
from loguru import logger
from pydantic import BaseModel
import nonebot
class AlibabaGreenPluginConfig(BaseModel):
module_aligreen_enable: bool = False
module_aligreen_access_key_id: str = ""
module_aligreen_access_key_secret: str = ""
module_aligreen_region_id: str = "cn-shenzhen"
module_aligreen_endpoint: str = "green-cip.cn-shenzhen.aliyuncs.com"
module_aligreen_service: str = "llm_query_moderation"
class AlibabaGreen:
_client: AlibabaGreenClient | None = None
_config: AlibabaGreenPluginConfig | None = None
@staticmethod
def get_client() -> AlibabaGreenClient:
assert AlibabaGreen._client is not None
return AlibabaGreen._client
@staticmethod
def get_config() -> AlibabaGreenPluginConfig:
assert AlibabaGreen._config is not None
return AlibabaGreen._config
@staticmethod
def init():
config = nonebot.get_plugin_config(AlibabaGreenPluginConfig)
AlibabaGreen._config = config
if not config.module_aligreen_enable:
logger.info("该环境未启用阿里内容审查,跳过初始化")
return
AlibabaGreen._client = AlibabaGreenClient(AlibabaTeaConfig(
access_key_id=config.module_aligreen_access_key_id,
access_key_secret=config.module_aligreen_access_key_secret,
connect_timeout=10000,
read_timeout=3000,
region_id=config.module_aligreen_region_id,
endpoint=config.module_aligreen_endpoint,
))
@staticmethod
def _detect_sync(content: str) -> bool:
if not AlibabaGreen.get_config().module_aligreen_enable:
logger.debug("该环境未启用阿里内容审查,直接跳过")
return True
client = AlibabaGreen.get_client()
try:
response = client.text_moderation_plus(TextModerationPlusRequest(
service=AlibabaGreen.get_config().module_aligreen_service,
service_parameters=json.dumps({
"content": content,
}),
))
if response.status_code == 200:
result = response.body
logger.info(f"检测违规内容 API 调用成功:{result}")
risk_level: str = result.data.risk_level or "none"
if risk_level == "high":
return False
return True
logger.error(f"检测违规内容 API 调用失败:{response}")
return True
except Exception as e:
logger.error("检测违规内容 API 调用失败")
logger.exception(e)
return True
@staticmethod
async def detect(content: str) -> bool:
return await asyncio.to_thread(AlibabaGreen._detect_sync, content)
driver = nonebot.get_driver()
@driver.on_startup
async def _():
AlibabaGreen.init()

View File

@ -59,6 +59,9 @@ 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 未配置,该功能无法使用")
if llm_config.default_llm in llm_config.llms:
logger.warning(f"[LLM] 需求的 LLM 不存在,回退到默认模型 REQUIRED={llm_model}")
return llm_config.llms[llm_config.default_llm]
raise NotImplementedError("[LLM] LLM 未配置,该功能无法使用")
return llm_config.llms[llm_model]

View File

@ -0,0 +1,3 @@
# 已废弃
坏枪用简单的 LLM + 提示词工程,完成了这 200 块的 `qwen3-coder-plus` 都搞不定的 nb 功能

View File

@ -9,7 +9,6 @@ import datetime
from typing import Optional
from .expression import TimeExpression
from .err import TokenUnhandledException, MultipleSpecificationException
def parse(text: str, now: Optional[datetime.datetime] = None) -> datetime.datetime:
@ -56,19 +55,4 @@ class Parser:
TokenUnhandledException: If the input cannot be parsed
"""
return TimeExpression.parse(text, self.now)
def digest_chinese_number(self, text: str) -> tuple[str, int]:
"""
Parse a Chinese number from the beginning of text and return the rest and the parsed number.
This matches the interface of the original digest_chinese_number method.
Args:
text: Text that may start with a Chinese number
Returns:
Tuple of (remaining_text, parsed_number)
"""
from .chinese_number import ChineseNumberParser
parser = ChineseNumberParser()
return parser.digest(text)

View File

@ -2,10 +2,9 @@
Abstract Syntax Tree (AST) nodes for the time expression parser.
"""
from abc import ABC, abstractmethod
from typing import Optional, List
from abc import ABC
from typing import Optional
from dataclasses import dataclass
import datetime
@dataclass
@ -69,4 +68,4 @@ class TimeExpressionNode(ASTNode):
time: Optional[TimeNode] = None
relative_date: Optional[RelativeDateNode] = None
relative_time: Optional[RelativeTimeNode] = None
weekday: Optional[WeekdayNode] = None
weekday: Optional[WeekdayNode] = None

View File

@ -10,7 +10,7 @@ from .ptime_ast import (
TimeExpressionNode, DateNode, TimeNode,
RelativeDateNode, RelativeTimeNode, WeekdayNode, NumberNode
)
from .err import TokenUnhandledException, MultipleSpecificationException
from .err import TokenUnhandledException
class SemanticAnalyzer:
@ -366,4 +366,4 @@ class SemanticAnalyzer:
smart_time = self.infer_smart_time(time.hour, time.minute, time.second, base_time=result)
result = smart_time
return result
return result

51
konabot/docs/user/fx.txt Normal file
View File

@ -0,0 +1,51 @@
## 指令介绍
`fx` - 用于对图片应用各种滤镜效果的指令
## 格式
```
fx [滤镜名称] <参数1> <参数2> ...
```
## 示例
- `fx 模糊`
- `fx 阈值 150`
- `fx 缩放 2.0`
- `fx 色彩 1.8`
- `fx 色键 rgb(0,255,0) 50`
## 可用滤镜列表
### 基础滤镜
* ```fx 模糊 <半径=10>```
* ```fx 马赛克 <像素大小=10>```
* ```fx 轮廓```
* ```fx 锐化```
* ```fx 边缘增强```
* ```fx 浮雕```
* ```fx 查找边缘```
* ```fx 平滑```
### 色彩处理滤镜
* ```fx 反色```
* ```fx 黑白```
* ```fx 阈值 <阈值=128>```
* ```fx 对比度 <因子=1.5>```
* ```fx 亮度 <因子=1.5>```
* ```fx 色彩 <因子=1.5>```
* ```fx 色调 <颜色="rgb(255,0,0)">```
### 几何变换滤镜
* ```fx 缩放 <比例=1.5>```
* ```fx 波纹 <振幅=5> <波长=20>```
### 特殊效果滤镜
* ```fx 色键 <目标颜色="rgb(255,0,0)"> <容差=60>```
## 颜色名称支持
- **基本颜色**:红、绿、蓝、黄、紫、黑、白、橙、粉、灰、青、靛、棕
- **修饰词**:浅、深、亮、暗(可组合使用,如`浅红`、`深蓝`
- **RGB格式**`rgb(255,0,0)`、`rgb(0,255,0)`、`(255,0,0)` 等
- **HEX格式**`#66ccff`等

View File

@ -0,0 +1,24 @@
# tqszm
引用一条消息,让此方帮你提取首字母。
例子:
```
John: 11-28 16:50:37
谁来总结一下今天的工作?
Jack: 11-28 16:50:55
[引用John的消息] tqszm
此方Bot: 11-28 16:50:56
slzjyxjtdgz
```
或者,你也可以直接以正常指令的方式调用:
```
提取首字母 中山大学
> zsdx
```

View File

@ -6,34 +6,25 @@ from nonebot_plugin_alconna import Reference, Reply, UniMsg
from nonebot.adapters import Event
matcher_fix = on_message()
pattern = (
r"^(?:(?:av|cv)\d+|BV[a-zA-Z0-9]{10})|"
r"(?:b23\.tv|bili(?:22|23|33|2233)\.cn|\.bilibili\.com|QQ小程序(?:&amp;#93;|&#93;|\])哔哩哔哩).{0,500}"
)
@matcher_fix.handle()
async def _(msg: UniMsg, event: Event):
def _rule(msg: UniMsg):
to_search = msg.exclude(Reply, Reference).dump(json=True)
to_search2 = msg.exclude(Reply, Reference).extract_plain_text()
if not re.search(pattern, to_search) and not re.search(pattern, to_search2):
return
return False
return True
matcher_fix = on_message(rule=_rule)
@matcher_fix.handle()
async def _(event: Event):
from nonebot_plugin_analysis_bilibili import handle_analysis
await handle_analysis(event)
# b_url: str
# b_page: str | None
# b_time: str | None
#
# from nonebot_plugin_analysis_bilibili.analysis_bilibili import extract as bilibili_extract
#
# b_url, b_page, b_time = bilibili_extract(to_search)
# if b_url is None:
# return
#
# await matcher_fix.send(await UniMessage().text(b_url).export())

View File

@ -0,0 +1,58 @@
from io import BytesIO
from inspect import signature
from konabot.common.nb.extract_image import DepPILImage
from nonebot.adapters import Event as BaseEvent
from nonebot import on_message
from nonebot_plugin_alconna import (
UniMessage,
UniMsg
)
from konabot.plugins.fx_process.fx_manager import ImageFilterManager
def is_fx_mentioned(evt: BaseEvent, msg: UniMsg) -> bool:
txt = msg.extract_plain_text()
if "fx" not in txt[:3]:
return False
return True
fx_on = on_message(rule=is_fx_mentioned)
@fx_on.handle()
async def _(msg: UniMsg, event: BaseEvent, img: DepPILImage):
args = msg.extract_plain_text().split()
if len(args) < 2:
return
filter_name = args[1]
filter_func = ImageFilterManager.get_filter(filter_name)
if not filter_func:
return
# 获取函数最大参数数量
sig = signature(filter_func)
max_params = len(sig.parameters) - 1 # 减去第一个参数 image
# 从 args 提取参数,并转换为适当类型
func_args = []
for i in range(2, min(len(args), max_params + 2)):
# 尝试将参数转换为函数签名中对应的类型
param = list(sig.parameters.values())[i - 1]
param_type = param.annotation
arg_value = args[i]
try:
if param_type is float:
converted_value = float(arg_value)
elif param_type is int:
converted_value = int(arg_value)
else:
converted_value = arg_value
except Exception:
converted_value = arg_value
func_args.append(converted_value)
# 应用滤镜
out_img = filter_func(img, *func_args)
output = BytesIO()
out_img.save(output, format="PNG")
await fx_on.send(await UniMessage().image(raw=output).export())

View File

@ -0,0 +1,50 @@
from typing import Optional
from PIL import ImageColor
class ColorHandle:
color_name_map = {
"": (255, 0, 0),
"绿": (0, 255, 0),
"": (0, 0, 255),
"": (255, 255, 0),
"": (128, 0, 128),
"": (0, 0, 0),
"": (255, 255, 255),
"": (255, 165, 0),
"": (255, 192, 203),
"": (128, 128, 128),
"": (0, 255, 255),
"": (75, 0, 130),
"": (165, 42, 42),
"": (200, 200, 200),
"": (50, 50, 50),
"": (255, 255, 224),
"": (47, 79, 79),
}
@staticmethod
def set_or_blend_color(ori_color: Optional[tuple], target_color: tuple) -> tuple:
# 如果没有指定初始颜色,返回目标颜色
if ori_color is None:
return target_color
# 混合颜色,取平均值
blended_color = tuple((o + t) // 2 for o, t in zip(ori_color, target_color))
return blended_color
@staticmethod
def parse_color(color_str: str) -> tuple:
# 如果是纯括号,则加上前缀 rgb
if color_str.startswith('(') and color_str.endswith(')'):
color_str = 'rgb' + color_str
try:
return ImageColor.getrgb(color_str)
except ValueError:
pass
base_color = None
color_str = color_str.replace('', '')
for name, rgb in ColorHandle.color_name_map.items():
if name in color_str:
base_color = ColorHandle.set_or_blend_color(base_color, rgb)
if base_color is not None:
return base_color
return (255, 255, 255) # 默认白色

View File

@ -0,0 +1,167 @@
from PIL import Image, ImageFilter
from PIL import ImageEnhance
from konabot.plugins.fx_process.color_handle import ColorHandle
import math
class ImageFilterImplement:
@staticmethod
def apply_blur(image: Image.Image, radius: float = 10) -> Image.Image:
return image.filter(ImageFilter.GaussianBlur(radius))
# 马赛克
@staticmethod
def apply_mosaic(image: Image.Image, pixel_size: int = 10) -> Image.Image:
if pixel_size <= 0:
pixel_size = 1
# 缩小图像
small_image = image.resize(
(image.width // pixel_size, image.height // pixel_size),
Image.Resampling.NEAREST
)
# 放大图像
return small_image.resize(image.size, Image.Resampling.NEAREST)
@staticmethod
def apply_contour(image: Image.Image) -> Image.Image:
return image.filter(ImageFilter.CONTOUR)
@staticmethod
def apply_sharpen(image: Image.Image) -> Image.Image:
return image.filter(ImageFilter.SHARPEN)
@staticmethod
def apply_edge_enhance(image: Image.Image) -> Image.Image:
return image.filter(ImageFilter.EDGE_ENHANCE)
@staticmethod
def apply_emboss(image: Image.Image) -> Image.Image:
return image.filter(ImageFilter.EMBOSS)
@staticmethod
def apply_find_edges(image: Image.Image) -> Image.Image:
return image.filter(ImageFilter.FIND_EDGES)
@staticmethod
def apply_smooth(image: Image.Image) -> Image.Image:
return image.filter(ImageFilter.SMOOTH)
# 反色
@staticmethod
def apply_invert(image: Image.Image) -> Image.Image:
# 确保图像是RGBA模式保留透明度通道
if image.mode != 'RGBA':
image = image.convert('RGBA')
r, g, b, a = image.split()
r = r.point(lambda i: 255 - i)
g = g.point(lambda i: 255 - i)
b = b.point(lambda i: 255 - i)
return Image.merge('RGBA', (r, g, b, a))
# 黑白灰度
@staticmethod
def apply_black_white(image: Image.Image) -> Image.Image:
# 保留透明度通道
if image.mode != 'RGBA':
image = image.convert('RGBA')
r, g, b, a = image.split()
gray = Image.merge('RGB', (r, g, b)).convert('L')
return Image.merge('RGBA', (gray, gray, gray, a))
# 阈值
@staticmethod
def apply_threshold(image: Image.Image, threshold: int = 128) -> Image.Image:
# 保留透明度通道
if image.mode != 'RGBA':
image = image.convert('RGBA')
r, g, b, a = image.split()
gray = Image.merge('RGB', (r, g, b)).convert('L')
bw = gray.point(lambda x: 255 if x >= threshold else 0, '1')
return Image.merge('RGBA', (bw.convert('L'), bw.convert('L'), bw.convert('L'), a))
# 对比度
@staticmethod
def apply_contrast(image: Image.Image, factor: float = 1.5) -> Image.Image:
enhancer = ImageEnhance.Contrast(image)
return enhancer.enhance(factor)
# 亮度
@staticmethod
def apply_brightness(image: Image.Image, factor: float = 1.5) -> Image.Image:
enhancer = ImageEnhance.Brightness(image)
return enhancer.enhance(factor)
# 色彩
@staticmethod
def apply_color(image: Image.Image, factor: float = 1.5) -> Image.Image:
enhancer = ImageEnhance.Color(image)
return enhancer.enhance(factor)
# 三色调
@staticmethod
def apply_to_color(image: Image.Image, color: str = 'rgb(255,0,0)') -> Image.Image:
if image.mode != 'RGBA':
image = image.convert('RGBA')
# 转为灰度图
gray = image.convert('L')
# 获取目标颜色的RGB值
rgb_color = ColorHandle.parse_color(color)
# 高光默认为白色,阴影默认为黑色
highlight = (255, 255, 255)
shadow = (0, 0, 0)
# 创建新的图像
new_image = Image.new('RGBA', image.size)
width, height = image.size
for x in range(width):
for y in range(height):
lum = gray.getpixel((x, y))
# 计算新颜色
new_r = int((rgb_color[0] * lum + shadow[0] * (highlight[0] - lum)) / 255)
new_g = int((rgb_color[1] * lum + shadow[1] * (highlight[1] - lum)) / 255)
new_b = int((rgb_color[2] * lum + shadow[2] * (highlight[2] - lum)) / 255)
a = image.getpixel((x, y))[3] # 保留原图的透明度
new_image.putpixel((x, y), (new_r, new_g, new_b, a))
return new_image
# 缩放
@staticmethod
def apply_resize(image: Image.Image, scale: float = 1.5) -> Image.Image:
if scale <= 0:
scale = 1.0
new_size = (int(image.width * scale), int(image.height * scale))
return image.resize(new_size, Image.Resampling.LANCZOS)
# 波纹
@staticmethod
def apply_wave(image: Image.Image, amplitude: float = 5, wavelength: float = 20) -> Image.Image:
width, height = image.size
new_image = Image.new('RGBA', (width, height))
for x in range(width):
for y in range(height):
offset_x = int(amplitude * math.sin(2 * math.pi * y / wavelength))
offset_y = int(amplitude * math.cos(2 * math.pi * x / wavelength))
new_x = x + offset_x
new_y = y + offset_y
if 0 <= new_x < width and 0 <= new_y < height:
new_image.putpixel((x, y), image.getpixel((new_x, new_y)))
else:
new_image.putpixel((x, y), (0, 0, 0, 0)) # 透明像素
return new_image
def apply_color_key(image: Image.Image, target_color: str = 'rgb(255,0,0)', tolerance: int = 60) -> Image.Image:
if image.mode != 'RGBA':
image = image.convert('RGBA')
target_rgb = ColorHandle.parse_color(target_color)
width, height = image.size
new_image = Image.new('RGBA', (width, height))
for x in range(width):
for y in range(height):
r, g, b, a = image.getpixel((x, y))
# 计算与目标颜色的距离
distance = math.sqrt((r - target_rgb[0]) ** 2 + (g - target_rgb[1]) ** 2 + (b - target_rgb[2]) ** 2)
if distance <= tolerance:
new_image.putpixel((x, y), (r, g, b, 0)) # 设置为透明
else:
new_image.putpixel((x, y), (r, g, b, a)) # 保留原像素
return new_image

View File

@ -0,0 +1,28 @@
from typing import Optional
from konabot.plugins.fx_process.fx_handle import ImageFilterImplement
class ImageFilterManager:
filter_map = {
"模糊": ImageFilterImplement.apply_blur,
"马赛克": ImageFilterImplement.apply_mosaic,
"轮廓": ImageFilterImplement.apply_contour,
"锐化": ImageFilterImplement.apply_sharpen,
"边缘增强": ImageFilterImplement.apply_edge_enhance,
"浮雕": ImageFilterImplement.apply_emboss,
"查找边缘": ImageFilterImplement.apply_find_edges,
"平滑": ImageFilterImplement.apply_smooth,
"反色": ImageFilterImplement.apply_invert,
"黑白": ImageFilterImplement.apply_black_white,
"阈值": ImageFilterImplement.apply_threshold,
"对比度": ImageFilterImplement.apply_contrast,
"亮度": ImageFilterImplement.apply_brightness,
"色彩": ImageFilterImplement.apply_color,
"色调": ImageFilterImplement.apply_to_color,
"缩放": ImageFilterImplement.apply_resize,
"波纹": ImageFilterImplement.apply_wave,
"色键": ImageFilterImplement.apply_color_key,
}
@classmethod
def get_filter(cls, name: str) -> Optional[callable]:
return cls.filter_map.get(name)

View File

@ -10,7 +10,9 @@ from nonebot_plugin_alconna import (Alconna, Args, UniMessage, UniMsg,
on_alconna)
from nonebot_plugin_apscheduler import scheduler
from konabot.common import username
from konabot.common.longtask import DepLongTaskTarget
from konabot.common.pager import PagerQuery
from konabot.plugins.kona_ph.core.message import (get_daily_report,
get_daily_report_v2,
get_puzzle_description,
@ -76,7 +78,7 @@ async def _(target: DepLongTaskTarget):
cmd_history = on_alconna(Alconna(
"历史题目",
"re:历史(题目|谜题)",
Args["page?", int],
Args["index_id?", str],
), rule=to_me())
@ -118,6 +120,24 @@ async def _(target: DepLongTaskTarget, index_id: str = "", page: int = 1):
await target.send_message(msg)
cmd_leadboard = on_alconna(Alconna(
"re:此方(解谜|谜题)排行榜",
Args["page?", int],
))
@cmd_leadboard.handle()
async def _(target: DepLongTaskTarget, page: int = 1):
async with puzzle_manager() as manager:
result = manager.get_leadboard(PagerQuery(page, 10))
await target.send_message(result.to_unimessage(
title="此方解谜排行榜",
formatter=lambda data: (
f"{data[1]} 已完成 | "
f"{username.get_username(data[0])}"
)
))
@scheduler.scheduled_job("cron", hour="8")
async def _():
async with puzzle_manager() as manager:

View File

@ -17,7 +17,7 @@ class PuzzleImageManager:
21,
)
img_name = f"{id}{suffix}"
(KONAPH_IMAGE_BASE / img_name).write_bytes(data)
_ = (KONAPH_IMAGE_BASE / img_name).write_bytes(data)
return img_name
def remove_puzzle_image(self, img_name: str):

View File

@ -33,7 +33,7 @@ def get_puzzle_description(puzzle: Puzzle, with_answer: bool = False) -> UniMess
)
result = result.text(f"\n\n出题者:{get_username(puzzle.author_id)}")
if with_answer:
result = result.text(f"\n\n题目答案:{puzzle.flag}")
else:

View File

@ -1,5 +1,6 @@
import asyncio
import datetime
import functools
import random
import re
from contextlib import asynccontextmanager
@ -7,6 +8,7 @@ from contextlib import asynccontextmanager
import nanoid
from pydantic import BaseModel, Field, ValidationError
from konabot.common.pager import PagerQuery
from konabot.plugins.kona_ph.core.path import KONAPH_DATA_JSON
@ -112,6 +114,10 @@ class PuzzleManager(BaseModel):
index_id_counter: int = 1
submissions: dict[str, dict[str, list[PuzzleSubmission]]] = {}
"""
类型:{ [raw_id: str]: { [user_id: str]: PuzzleSubmission[] } }
"""
last_checked_date: datetime.date = Field(
default_factory=lambda: get_today_date() - datetime.timedelta(days=1)
)
@ -235,6 +241,20 @@ class PuzzleManager(BaseModel):
if p.author_id == user
], key=lambda p: p.created_at, reverse=True)
def get_leadboard(self, pager: PagerQuery):
return pager.apply(sorted([
(user, sum((
len([
s for s in sl.get(user, [])
if s.success
]) for sl in self.submissions.values()
)))
for user in functools.reduce(
lambda x, y: x | y,
(set(sl.keys()) for sl in self.submissions.values()),
)
], key=lambda t: t[1], reverse=True))
lock = asyncio.Lock()

View File

@ -10,7 +10,6 @@ from pydantic import BaseModel
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.qq_broadcast import qq_broadcast
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,
@ -19,6 +18,7 @@ from konabot.plugins.kona_ph.core.message import (get_puzzle_description, get_pu
from konabot.plugins.kona_ph.core.storage import (Puzzle, PuzzleHint, PuzzleManager,
get_today_date,
puzzle_manager)
from konabot.plugins.poster.service import broadcast
PUZZLE_PAGE_SIZE = 10
@ -344,7 +344,7 @@ def create_admin_commands():
p = manager.get_today_puzzle(strong=True)
if p is None:
return await target.send_message("上架失败了orz可能是没题了")
await qq_broadcast(config.plugin_puzzle_playgroup, get_puzzle_description(p))
await broadcast("每日谜题", get_puzzle_description(p))
return await target.send_message("Ok!")
@cmd_admin.assign("preview")

View File

@ -1,9 +1,10 @@
from nonebot import on_message
from nonebot_plugin_alconna import UniMessage, UniMsg
from nonebot_plugin_alconna import UniMessage
evt = on_message()
from konabot.common.nb.match_keyword import match_keyword
evt = on_message(rule=match_keyword(""))
@evt.handle()
async def _(msg: UniMsg):
if msg.extract_plain_text() == "":
await evt.send(await UniMessage().text("").export())
async def _():
await evt.send(await UniMessage().text("").export())

View File

@ -1,9 +1,9 @@
import re
import aiohttp
import asyncio as asynkio
from math import ceil
from pathlib import Path
from typing import Any
import datetime
import nanoid
import nonebot
@ -14,9 +14,10 @@ from nonebot_plugin_alconna import Alconna, Args, Subcommand, UniMessage, UniMsg
from pydantic import BaseModel
from konabot.common.longtask import DepLongTaskTarget, LongTask, create_longtask, handle_long_task, longtask_data
from konabot.common.ptimeparse import parse
from konabot.common.nb.match_keyword import match_keyword
from konabot.plugins.simple_notify.ask_llm import ask_ai
evt = on_message()
evt = on_message(rule=match_keyword(re.compile("^.+提醒我.+$")))
(Path(__file__).parent.parent.parent.parent / "data").mkdir(exist_ok=True)
DATA_FILE_PATH = Path(__file__).parent.parent.parent.parent / "data" / "notify.json"
@ -76,21 +77,12 @@ async def _(msg: UniMsg, mEvt: Event, target: DepLongTaskTarget):
return
text = msg.extract_plain_text()
if "提醒我" not in text:
return
segments = text.split("提醒我", maxsplit=1)
if len(segments) != 2:
return
notify_time, notify_text = segments
try:
target_time = parse(notify_time)
logger.info(f"{notify_time} 解析出了时间:{target_time}")
except Exception:
logger.info(f"无法从 {notify_time} 中解析出时间")
return
if not notify_text:
target_time, notify_text = await ask_ai(text)
if target_time is None:
return
await create_longtask(

View File

@ -0,0 +1,158 @@
import datetime
import json
import re
from loguru import logger
from konabot.common.apis.ali_content_safety import AlibabaGreen
from konabot.common.llm import get_llm
SYSTEM_PROMPT = """你是一个专门解析提醒请求的助手。请分析用户输入识别其中是否包含提醒信息并输出标准化的JSON格式结果。
输入格式通常是:"xxxx提醒我yyyy",其中:
- xxxx 是用户提供的时间信息
- yyyy 是提醒内容。有些时候用户会有一些需求。你可以在合理的范围进行衍生。
输出要求:
- 必须是有效的JSON对象
- 包含以下字段:
* datetime: 如果是绝对时间填入ISO 8601格式的日期时间字符串否则为null
* datetime_delta: 如果是相对时间填入ISO 8601持续时间格式否则为null
* datetime_delta_minus: 如果时间偏移量是负数,则此项为 true否则为 false
* content: 提醒内容的字符串
* is_notice: 布尔值,表示这是否是真正的提醒请求
时间处理规则:
- 绝对时间示例:如果 xxxx 输入了非常明确的时间点,如"2024年12月25日" → 转换为具体datetime
- 相对时间示例:如果 xxxx 没有输入非常明确的时间点,如"10分钟后""2小时后""3天后" → 转换为datetime_delta
- 如果用户输入了需要计算的时间,你需要计算出正确的结果,如"10分钟后的8分钟前" → 转换为 “PT2M”
- zzzz 是系统提供的时间,每句话肯定都有,这不是你判断相对或绝对时间的依据,需严格按照 xxx 来判断
- datetime和datetime_delta有且仅有一个不为null
时间格式要求:
- datetime: "YYYY-MM-DDTHH:MM:SS" (ISO 8601)
- datetime_delta: "PxYxMxDTxHxMxS" 格式 (如"PT1H30M"表示1小时30分钟"P3DT4H"表示三天四小时,"P5MT2M"表示五个月两分钟)
判断标准:
- is_notice=true: 明确包含时间+提醒内容的请求
- is_notice=false: 闲聊、疑问句、或不符合提醒格式的内容
示例:
用户:"明天下午2点提醒我开会"
输出:{"datetime": "2024-01-16T14:00:00", "datetime_delta": null,
"datetime_delta_minus": false, "content": "开会", "is_notice": true}
用户:"5分钟后提醒我关火"
输出:{"datetime": null, "datetime_delta": "PT5M", "datetime_delta_minus": false, "content": "关火", "is_notice": true}
用户:"5分钟前提醒我关火"
输出:{"datetime": null, "datetime_delta": "PT5M", "datetime_delta_minus": true, "content": "关火", "is_notice": true}
用户:"五百年后提醒我关火"
输出:{"datetime": null, "datetime_delta": "P500Y", "datetime_delta_minus": false, "content": "关火", "is_notice": true}
用户:"昨天提醒我关火"
输出:{"datetime": null, "datetime_delta": "P1D", "datetime_delta_minus": true, "content": "关火", "is_notice": true}
用户:"什么是提醒功能?"
输出:{"datetime": null, "datetime_delta": null, "datetime_delta_minus": false, "content": "", "is_notice": false}
用户:"过一会会,用可爱的语气提醒我该睡觉了"
输出:{"datetime": null, "datetime_delta": "PT10M", "datetime_delta_minus": false, "content": "呼呼!该睡觉了哦!ヾ(•ω•`)o", "is_notice": true}
请严格按照上述格式输出JSON不要添加任何其他文字说明。现在是 DATETIME"""
pt_pattern = re.compile(
r"^P"
r"((?P<year>\d+)Y)?"
r"((?P<month>\d+)M)?"
r"((?P<day>\d+)D)?"
r"(T((?P<hour>\d+)H)?"
r"((?P<minute>\d+)M)?"
r"((?P<second>\d+)S)?)?$"
)
def tryint(s: str | None):
if s:
if re.match(r"^\d+$", s):
return int(s)
return 0
async def ask_ai(expression: str, now: datetime.datetime | None = None) -> tuple[datetime.datetime | None, str]:
if now is None:
now = datetime.datetime.now()
prompt = SYSTEM_PROMPT.replace("DATETIME", f"{now}, 星期 {now.weekday() + 1}")
is_safe = await AlibabaGreen.detect(expression)
if not is_safe:
logger.info(f"提醒功能:消息被阿里绿网拦截 message={expression}")
return None, ""
llm = get_llm("qwen3-max")
message = await llm.chat([
{ "role": "system", "content": prompt },
{ "role": "user", "content": expression },
])
result = message.content
if result is None:
return (None, "")
try:
data = json.loads(result)
except json.JSONDecodeError:
logger.info(f"提醒功能:解析 AI 返回值时出现问题 raw={result}")
return (None, "")
datetime_absolute = data.get("datetime", None)
datetime_delta = data.get("datetime_delta", None)
is_minus = data.get("datetime_delta_minus", False)
content = data.get("content", "")
is_notice = data.get("is_notice", False)
if not is_notice:
return (None, "")
if datetime_absolute:
try:
res = datetime.datetime.strptime(datetime_absolute, "%Y-%m-%dT%H:%M:%S"), content
logger.info(f"提醒功能:使用绝对时间解析 AI 返回值 raw={result} target={res[0]}")
return res
except ValueError:
pass
if datetime_delta and (match := pt_pattern.match(datetime_delta)):
years = tryint(match.group("year"))
months = tryint(match.group("month"))
days = tryint(match.group("day"))
hours = tryint(match.group("hour"))
minutes = tryint(match.group("minute"))
seconds = tryint(match.group("second"))
dt = datetime.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)
if is_minus:
dt = -dt
if is_minus:
now2 = now.replace(year=now.year - years)
m = now2.month
if (months - m) >= 0:
neg_months = -(m - months - 1)
neg_years = (neg_months + 11) // 12
target_month = 12 - ((neg_months - 1) % 12)
now2 = now2.replace(year=now2.year - neg_years, month=target_month)
else:
now2 = now2.replace(month=m - months)
else:
now2 = now.replace(year=now.year + years)
m = now2.month
now2 = now2.replace(
year=now2.year + (m + months - 1) // 12,
month=(m + months - 1) % 12 + 1
)
logger.info(f"提醒功能:使用相对时间解析 AI 返回值 raw={result} target={now2+dt}")
return (now2 + dt, content)
logger.warning(f"提醒功能:解析 AI 返回值时没有找到解析方法 raw={result}")
return (None, "")

View File

@ -0,0 +1,71 @@
"""
提取首字母
...谁需要啊!!!
"""
import functools
from loguru import logger
from nonebot import on_message
from nonebot.rule import KeywordsRule, Rule, ToMeRule
from nonebot.adapters.onebot.v11.bot import Bot as OBBot
from nonebot.adapters.onebot.v11.event import MessageEvent as OBMessageEvent
from nonebot.adapters.onebot.v11.message import MessageSegment as OBMessageSegment
from nonebot_plugin_alconna import Text, UniMsg
from nonebot_plugin_alconna.uniseg.adapters.onebot11.builder import Onebot11MessageBuilder
from pypinyin import pinyin, Style as PinyinStyle
from konabot.common.longtask import DepLongTaskTarget
from konabot.common.apis.ali_content_safety import AlibabaGreen
keywords = ("szmtq", "tqszm", "提取首字母", "首字母提取", )
cmd_tqszm = on_message(rule=Rule(ToMeRule(), KeywordsRule(*keywords)))
@cmd_tqszm.handle()
async def _(target: DepLongTaskTarget, msg: UniMsg, evt: OBMessageEvent | None = None, bot: OBBot | None = None):
texts = ""
command_occurred = False
is_reply_mode = False
if evt is not None and bot is not None:
reply = await Onebot11MessageBuilder().extract_reply(evt, bot)
if reply is not None:
is_reply_mode = True
for seg in reply.data.get('msg', []):
if isinstance(seg, OBMessageSegment) and seg.type == 'text':
segtext: str = seg.data.get('text', '')
texts += segtext
if is_reply_mode:
for seg in msg:
if isinstance(seg, Text):
if all(seg.text.strip() != k for k in keywords):
return
else:
for seg in msg:
if isinstance(seg, Text):
if command_occurred:
texts += seg.text
continue
if any(seg.text.startswith(w) for w in keywords):
command_occurred = True
texts += next(
seg.text.removeprefix(w) for w in keywords
if seg.text.startswith(w)
).removeprefix(" ")
result = pinyin(texts, style=PinyinStyle.FIRST_LETTER, errors=lambda x: x)
print(result)
result_text = functools.reduce(lambda x, y: x + y, [
p[0] for p in result if len(p) > 0
], "")
if not await AlibabaGreen.detect(result_text):
logger.info(f"对首字母序列的检测未通过安全测试 RAW={texts} FORMATTED={result_text}")
await target.send_message(result_text, at=False)

304
poetry.lock generated
View File

@ -20,6 +20,23 @@ type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "mirrors"
[[package]]
name = "aiofiles"
version = "24.1.0"
description = "File support for asyncio."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"},
{file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"},
]
[package.source]
type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "mirrors"
[[package]]
name = "aiohappyeyeballs"
version = "2.6.1"
@ -233,6 +250,187 @@ type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "mirrors"
[[package]]
name = "alibabacloud-credentials"
version = "1.0.3"
description = "The alibabacloud credentials module of alibabaCloud Python SDK."
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "alibabacloud-credentials-1.0.3.tar.gz", hash = "sha256:9d8707e96afc6f348e23f5677ed15a21c2dfce7cfe6669776548ee4c80e1dfaf"},
{file = "alibabacloud_credentials-1.0.3-py3-none-any.whl", hash = "sha256:30c8302f204b663c655d97e1c283ee9f9f84a6257d7901b931477d6cf34445a8"},
]
[package.dependencies]
aiofiles = ">=22.1.0,<25.0.0"
alibabacloud-credentials-api = ">=1.0.0,<2.0.0"
alibabacloud-tea = ">=0.4.0"
APScheduler = ">=3.10.0,<4.0.0"
[package.source]
type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "mirrors"
[[package]]
name = "alibabacloud-credentials-api"
version = "1.0.0"
description = "Alibaba Cloud Gateway SPI SDK Library for Python"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "alibabacloud-credentials-api-1.0.0.tar.gz", hash = "sha256:8c340038d904f0218d7214a8f4088c31912bfcf279af2cbc7d9be4897a97dd2f"},
]
[package.source]
type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "mirrors"
[[package]]
name = "alibabacloud-endpoint-util"
version = "0.0.4"
description = "The endpoint-util module of alibabaCloud Python SDK."
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "alibabacloud_endpoint_util-0.0.4.tar.gz", hash = "sha256:a593eb8ddd8168d5dc2216cd33111b144f9189fcd6e9ca20e48f358a739bbf90"},
]
[package.source]
type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "mirrors"
[[package]]
name = "alibabacloud-gateway-spi"
version = "0.0.3"
description = "Alibaba Cloud Gateway SPI SDK Library for Python"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "alibabacloud_gateway_spi-0.0.3.tar.gz", hash = "sha256:10d1c53a3fc5f87915fbd6b4985b98338a776e9b44a0263f56643c5048223b8b"},
]
[package.dependencies]
alibabacloud_credentials = ">=0.3.4"
[package.source]
type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "mirrors"
[[package]]
name = "alibabacloud-green20220302"
version = "3.0.1"
description = "Alibaba Cloud Green (20220302) SDK Library for Python"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "alibabacloud_green20220302-3.0.1-py3-none-any.whl", hash = "sha256:01cd4a402f8de45423cf5c52c59fc696255ef5f291c3e1e10ac4985033e3e1d9"},
{file = "alibabacloud_green20220302-3.0.1.tar.gz", hash = "sha256:083ad03e7c553ec127b69ad5b039f42aaac10730784df319ead2f8a0d8ee928a"},
]
[package.dependencies]
alibabacloud-endpoint-util = ">=0.0.4,<1.0.0"
alibabacloud-openapi-util = ">=0.2.2,<1.0.0"
alibabacloud-tea-openapi = ">=0.3.16,<1.0.0"
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
[package.source]
type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "mirrors"
[[package]]
name = "alibabacloud-openapi-util"
version = "0.2.2"
description = "Aliyun Tea OpenApi Library for Python"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "alibabacloud_openapi_util-0.2.2.tar.gz", hash = "sha256:ebbc3906f554cb4bf8f513e43e8a33e8b6a3d4a0ef13617a0e14c3dda8ef52a8"},
]
[package.dependencies]
alibabacloud_tea_util = ">=0.0.2"
cryptography = ">=3.0.0"
[package.source]
type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "mirrors"
[[package]]
name = "alibabacloud-tea"
version = "0.4.3"
description = "The tea module of alibabaCloud Python SDK."
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "alibabacloud-tea-0.4.3.tar.gz", hash = "sha256:ec8053d0aa8d43ebe1deb632d5c5404339b39ec9a18a0707d57765838418504a"},
]
[package.dependencies]
aiohttp = ">=3.7.0,<4.0.0"
requests = ">=2.21.0,<3.0.0"
[package.source]
type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "mirrors"
[[package]]
name = "alibabacloud-tea-openapi"
version = "0.4.2"
description = "Alibaba Cloud openapi SDK Library for Python"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "alibabacloud_tea_openapi-0.4.2-py3-none-any.whl", hash = "sha256:c498065a297fd1972ed7709ef935c9ce1f9757f267f68933de6e63853f37366f"},
{file = "alibabacloud_tea_openapi-0.4.2.tar.gz", hash = "sha256:0a8d79374ca692469472355a125969c8a22cc5fb08328c75c26663ccf5c8b168"},
]
[package.dependencies]
alibabacloud-credentials = ">=1.0.2,<2.0.0"
alibabacloud-gateway-spi = ">=0.0.2,<1.0.0"
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
cryptography = ">=3.0.0,<45.0.0"
darabonba-core = ">=1.0.3,<2.0.0"
[package.source]
type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "mirrors"
[[package]]
name = "alibabacloud-tea-util"
version = "0.3.14"
description = "The tea-util module of alibabaCloud Python SDK."
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "alibabacloud_tea_util-0.3.14-py3-none-any.whl", hash = "sha256:10d3e5c340d8f7ec69dd27345eb2fc5a1dab07875742525edf07bbe86db93bfe"},
{file = "alibabacloud_tea_util-0.3.14.tar.gz", hash = "sha256:708e7c9f64641a3c9e0e566365d2f23675f8d7c2a3e2971d9402ceede0408cdb"},
]
[package.dependencies]
alibabacloud-tea = ">=0.3.3"
[package.source]
type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "mirrors"
[[package]]
name = "annotated-doc"
version = "0.0.3"
@ -982,6 +1180,93 @@ type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "mirrors"
[[package]]
name = "cryptography"
version = "44.0.3"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
python-versions = ">=3.7, !=3.9.0, !=3.9.1"
groups = ["main"]
files = [
{file = "cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88"},
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137"},
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c"},
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76"},
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359"},
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43"},
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01"},
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d"},
{file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904"},
{file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44"},
{file = "cryptography-44.0.3-cp37-abi3-win32.whl", hash = "sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d"},
{file = "cryptography-44.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d"},
{file = "cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f"},
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759"},
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645"},
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2"},
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54"},
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93"},
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c"},
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f"},
{file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5"},
{file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b"},
{file = "cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028"},
{file = "cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334"},
{file = "cryptography-44.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cad399780053fb383dc067475135e41c9fe7d901a97dd5d9c5dfb5611afc0d7d"},
{file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:21a83f6f35b9cc656d71b5de8d519f566df01e660ac2578805ab245ffd8523f8"},
{file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fc3c9babc1e1faefd62704bb46a69f359a9819eb0292e40df3fb6e3574715cd4"},
{file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:e909df4053064a97f1e6565153ff8bb389af12c5c8d29c343308760890560aff"},
{file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dad80b45c22e05b259e33ddd458e9e2ba099c86ccf4e88db7bbab4b747b18d06"},
{file = "cryptography-44.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:479d92908277bed6e1a1c69b277734a7771c2b78633c224445b5c60a9f4bc1d9"},
{file = "cryptography-44.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:896530bc9107b226f265effa7ef3f21270f18a2026bc09fed1ebd7b66ddf6375"},
{file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9b4d4a5dbee05a2c390bf212e78b99434efec37b17a4bff42f50285c5c8c9647"},
{file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259"},
{file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dd3db61b8fe5be220eee484a17233287d0be6932d056cf5738225b9c05ef4fff"},
{file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:978631ec51a6bbc0b7e58f23b68a8ce9e5f09721940933e9c217068388789fe5"},
{file = "cryptography-44.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5d20cc348cca3a8aa7312f42ab953a56e15323800ca3ab0706b8cd452a3a056c"},
{file = "cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053"},
]
[package.dependencies]
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
[package.extras]
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0) ; python_version >= \"3.8\""]
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""]
pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
sdist = ["build (>=1.0.0)"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi (>=2024)", "cryptography-vectors (==44.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
test-randomorder = ["pytest-randomly"]
[package.source]
type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "mirrors"
[[package]]
name = "darabonba-core"
version = "1.0.4"
description = "The darabonba module of alibabaCloud Python SDK."
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "darabonba_core-1.0.4-py3-none-any.whl", hash = "sha256:4c3bc1d76d5af1087297b6afde8e960ea2f54f93e725e2df8453f0b4bb27dd24"},
{file = "darabonba_core-1.0.4.tar.gz", hash = "sha256:6ede4e9bfd458148bab19ab2331716ae9b5c226ba5f6d221de6f88ee65704137"},
]
[package.dependencies]
aiohttp = ">=3.7.0,<4.0.0"
alibabacloud-tea = "*"
requests = ">=2.21.0,<3.0.0"
[package.source]
type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "mirrors"
[[package]]
name = "distro"
version = "1.9.0"
@ -3454,6 +3739,23 @@ type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "mirrors"
[[package]]
name = "pypinyin"
version = "0.55.0"
description = "汉字拼音转换模块/工具."
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4"
groups = ["main"]
files = [
{file = "pypinyin-0.55.0-py2.py3-none-any.whl", hash = "sha256:d53b1e8ad2cdb815fb2cb604ed3123372f5a28c6f447571244aca36fc62a286f"},
{file = "pypinyin-0.55.0.tar.gz", hash = "sha256:b5711b3a0c6f76e67408ec6b2e3c4987a3a806b7c528076e7c7b86fcf0eaa66b"},
]
[package.source]
type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "mirrors"
[[package]]
name = "pytest"
version = "9.0.1"
@ -4681,4 +4983,4 @@ reference = "mirrors"
[metadata]
lock-version = "2.1"
python-versions = ">=3.12,<4.0"
content-hash = "2c341fdc0d5b29ad3b24516c46e036b2eff4c11e244047d114971039255c2ac4"
content-hash = "59498c038a603c90f051d2f360cb9226ec0fc4470942c0a7cf34f832701f0ce7"

View File

@ -29,6 +29,8 @@ dependencies = [
"imageio (>=2.37.2,<3.0.0)",
"aiosqlite (>=0.20.0,<1.0.0)",
"sqlparse (>=0.5.0,<1.0.0)",
"alibabacloud-green20220302 (>=3.0.1,<4.0.0)",
"pypinyin (>=0.55.0,<0.56.0)",
]
[tool.poetry]