Compare commits
50 Commits
fx_storage
...
00c0202720
| Author | SHA1 | Date | |
|---|---|---|---|
| 00c0202720 | |||
|
3ddf81e7de
|
|||
|
ba15841836
|
|||
|
014e9c9a71
|
|||
| 32cabc9452 | |||
|
19e83dea01
|
|||
|
9210f85300
|
|||
|
74052594c3
|
|||
|
31ad8dac3e
|
|||
|
c46b88060b
|
|||
|
02018cd11d
|
|||
|
d4cde42bdc
|
|||
|
58ff8f02da
|
|||
|
b32ddcaf38
|
|||
|
1eb7e62cfe
|
|||
|
c44e29a907
|
|||
|
24457ff7cd
|
|||
|
0d36bea3ca
|
|||
|
bf8504d432
|
|||
|
16a55ae69a
|
|||
|
3adbd38d65
|
|||
|
420630e35c
|
|||
|
36a564547c
|
|||
|
eb8bf16346
|
|||
| 67884f7133 | |||
| f18d94670e | |||
| 6e86a6987f | |||
| 9c9496efbd | |||
| 770d7567fb | |||
| 7026337a43 | |||
|
ef617e1c85
|
|||
|
bd71a8d75f
|
|||
| 605407549b | |||
| 5e01e086f2 | |||
| 1f887aeaf6 | |||
| 5de4b72a6b | |||
| 1861cd4f1a | |||
| 9148073095 | |||
|
ef3404b096
|
|||
| 14feae943e | |||
| 1d763dfc3c | |||
| a829f035b3 | |||
| 9904653cc6 | |||
| de04fcbec1 | |||
| 70e3565e44 | |||
| 6b10c99c7a | |||
|
cdfb822f42
|
|||
|
73aad89f57
|
|||
|
e1b5f9cfc9
|
|||
| 35f411fb3a |
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +1,6 @@
|
|||||||
[submodule "assets/lexicon/THUOCL"]
|
[submodule "assets/lexicon/THUOCL"]
|
||||||
path = assets/lexicon/THUOCL
|
path = assets/lexicon/THUOCL
|
||||||
url = https://github.com/thunlp/THUOCL.git
|
url = https://github.com/thunlp/THUOCL.git
|
||||||
|
[submodule "assets/oracle"]
|
||||||
|
path = assets/oracle
|
||||||
|
url = https://gitea.service.jazzwhom.top/mttu-developers/oracle-source.git
|
||||||
|
|||||||
14
Dockerfile
14
Dockerfile
@ -1,3 +1,16 @@
|
|||||||
|
FROM alpine:latest AS artifacts
|
||||||
|
|
||||||
|
RUN apk add --no-cache curl xz
|
||||||
|
WORKDIR /tmp
|
||||||
|
|
||||||
|
RUN mkdir -p /artifacts
|
||||||
|
RUN curl -L -o typst.tar.xz "https://github.com/typst/typst/releases/download/v0.14.2/typst-x86_64-unknown-linux-musl.tar.xz" \
|
||||||
|
&& tar -xJf typst.tar.xz \
|
||||||
|
&& mv typst-x86_64-unknown-linux-musl/typst /artifacts
|
||||||
|
|
||||||
|
RUN chmod -R +x /artifacts/
|
||||||
|
|
||||||
|
|
||||||
FROM python:3.13-slim AS base
|
FROM python:3.13-slim AS base
|
||||||
|
|
||||||
ENV VIRTUAL_ENV=/app/.venv \
|
ENV VIRTUAL_ENV=/app/.venv \
|
||||||
@ -38,6 +51,7 @@ RUN uv sync --no-install-project
|
|||||||
FROM base AS runtime
|
FROM base AS runtime
|
||||||
|
|
||||||
COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
|
COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
|
||||||
|
COPY --from=artifacts /artifacts/ /usr/local/bin/
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
9856
assets/old_font/symtable.csv
Normal file
9856
assets/old_font/symtable.csv
Normal file
File diff suppressed because it is too large
Load Diff
1
assets/oracle
Submodule
1
assets/oracle
Submodule
Submodule assets/oracle added at 9f3c08c5d2
@ -51,6 +51,8 @@ class AlibabaGreen:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _detect_sync(content: str) -> bool:
|
def _detect_sync(content: str) -> bool:
|
||||||
|
if len(content) == 0:
|
||||||
|
return True
|
||||||
if not AlibabaGreen.get_config().module_aligreen_enable:
|
if not AlibabaGreen.get_config().module_aligreen_enable:
|
||||||
logger.debug("该环境未启用阿里内容审查,直接跳过")
|
logger.debug("该环境未启用阿里内容审查,直接跳过")
|
||||||
return True
|
return True
|
||||||
|
|||||||
112
konabot/common/artifact.py
Normal file
112
konabot/common/artifact.py
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
import hashlib
|
||||||
|
import platform
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import nonebot
|
||||||
|
from loguru import logger
|
||||||
|
from nonebot.adapters.discord.config import Config as DiscordConfig
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ArtifactDepends:
|
||||||
|
url: str
|
||||||
|
sha256: str
|
||||||
|
target: Path
|
||||||
|
|
||||||
|
required_os: str | None = None
|
||||||
|
"示例值:Windows, Linux, Darwin"
|
||||||
|
|
||||||
|
required_arch: str | None = None
|
||||||
|
"示例值:AMD64, x86_64, arm64"
|
||||||
|
|
||||||
|
use_proxy: bool = True
|
||||||
|
"网络问题,赫赫;使用的是 Discord 模块配置的 proxy"
|
||||||
|
|
||||||
|
def is_corresponding_platform(self) -> bool:
|
||||||
|
if self.required_os is not None:
|
||||||
|
if self.required_os.lower() != platform.system().lower():
|
||||||
|
return False
|
||||||
|
if self.required_arch is not None:
|
||||||
|
if self.required_arch.lower() != platform.machine().lower():
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class Config(BaseModel):
|
||||||
|
prefetch_artifact: bool = False
|
||||||
|
"是否提前下载好二进制依赖"
|
||||||
|
|
||||||
|
|
||||||
|
artifact_list = []
|
||||||
|
|
||||||
|
|
||||||
|
driver = nonebot.get_driver()
|
||||||
|
config = nonebot.get_plugin_config(Config)
|
||||||
|
|
||||||
|
@driver.on_startup
|
||||||
|
async def _():
|
||||||
|
if config.prefetch_artifact:
|
||||||
|
logger.info("启动检测中:正在检测需求的二进制是否下载")
|
||||||
|
semaphore = asyncio.Semaphore(10)
|
||||||
|
async def _task(artifact: ArtifactDepends):
|
||||||
|
async with semaphore:
|
||||||
|
await ensure_artifact(artifact)
|
||||||
|
|
||||||
|
tasks: set[asyncio.Task] = set()
|
||||||
|
for a in artifact_list:
|
||||||
|
tasks.add(asyncio.Task(_task(a)))
|
||||||
|
await asyncio.gather(*tasks, return_exceptions=False)
|
||||||
|
logger.info("检测好了")
|
||||||
|
|
||||||
|
|
||||||
|
async def download_artifact(artifact: ArtifactDepends):
|
||||||
|
proxy = None
|
||||||
|
if artifact.use_proxy:
|
||||||
|
discord_config = nonebot.get_plugin_config(DiscordConfig)
|
||||||
|
proxy = discord_config.discord_proxy
|
||||||
|
|
||||||
|
if proxy is not None:
|
||||||
|
logger.info(f"正在使用 Proxy 下载 TARGET={artifact.target} PROXY={proxy}")
|
||||||
|
else:
|
||||||
|
logger.info(f"正在下载 TARGET={artifact.target}")
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession(proxy=proxy) as client:
|
||||||
|
result = await client.get(artifact.url)
|
||||||
|
if result.status != 200:
|
||||||
|
logger.warning(f"已经下载了二进制,但是注意服务器没有返回 200! URL={artifact.url} TARGET={artifact.target} CODE={result.status}")
|
||||||
|
data = await result.read()
|
||||||
|
artifact.target.write_bytes(data)
|
||||||
|
if not platform.system().lower() == 'windows':
|
||||||
|
artifact.target.chmod(0o755)
|
||||||
|
|
||||||
|
logger.info(f"下载好了 TARGET={artifact.target} URL={artifact.url}")
|
||||||
|
m = hashlib.sha256(artifact.target.read_bytes())
|
||||||
|
if m.hexdigest().lower() != artifact.sha256.lower():
|
||||||
|
logger.warning(f"下载到的二进制的 sha256 与需求不同 TARGET={artifact.target} REQUESTED={artifact.sha256} ACTUAL={m.hexdigest()}")
|
||||||
|
|
||||||
|
|
||||||
|
async def ensure_artifact(artifact: ArtifactDepends):
|
||||||
|
if not artifact.is_corresponding_platform():
|
||||||
|
return
|
||||||
|
|
||||||
|
if not artifact.target.exists():
|
||||||
|
logger.info(f"二进制依赖 {artifact.target} 不存在")
|
||||||
|
if not artifact.target.parent.exists():
|
||||||
|
artifact.target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
await download_artifact(artifact)
|
||||||
|
else:
|
||||||
|
m = hashlib.sha256(artifact.target.read_bytes())
|
||||||
|
if m.hexdigest().lower() != artifact.sha256.lower():
|
||||||
|
logger.info(f"二进制依赖 {artifact.target} 的哈希无法对应需求的哈希,准备重新下载")
|
||||||
|
artifact.target.unlink()
|
||||||
|
await download_artifact(artifact)
|
||||||
|
|
||||||
|
|
||||||
|
def register_artifacts(*artifacts: ArtifactDepends):
|
||||||
|
artifact_list.extend(artifacts)
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
from typing import Any
|
from typing import Any, cast
|
||||||
import openai
|
import openai
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@ -26,14 +26,14 @@ class LLMInfo(BaseModel):
|
|||||||
|
|
||||||
async def chat(
|
async def chat(
|
||||||
self,
|
self,
|
||||||
messages: list[ChatCompletionMessageParam],
|
messages: list[ChatCompletionMessageParam] | list[dict[str, Any]],
|
||||||
timeout: float | None = 30.0,
|
timeout: float | None = 30.0,
|
||||||
max_tokens: int | None = None,
|
max_tokens: int | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> ChatCompletionMessage:
|
) -> ChatCompletionMessage:
|
||||||
logger.info(f"调用 LLM: BASE_URL={self.base_url} MODEL_NAME={self.model_name}")
|
logger.info(f"调用 LLM: BASE_URL={self.base_url} MODEL_NAME={self.model_name}")
|
||||||
completion: ChatCompletion = await self.get_openai_client().chat.completions.create(
|
completion: ChatCompletion = await self.get_openai_client().chat.completions.create(
|
||||||
messages=messages,
|
messages=cast(Any, messages),
|
||||||
model=self.model_name,
|
model=self.model_name,
|
||||||
max_tokens=max_tokens,
|
max_tokens=max_tokens,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
|
|||||||
@ -207,9 +207,21 @@ async def _ext_img(
|
|||||||
await matcher.send(await UniMessage.text(msg).export())
|
await matcher.send(await UniMessage.text(msg).export())
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
async def _try_ext_img(
|
||||||
|
evt: Event,
|
||||||
|
bot: Bot,
|
||||||
|
matcher: Matcher,
|
||||||
|
) -> bytes | None:
|
||||||
|
match await extract_image_data_from_message(evt.get_message(), evt, bot):
|
||||||
|
case Success(img):
|
||||||
|
return img
|
||||||
|
case Failure(err):
|
||||||
|
# raise BotExceptionMessage(err)
|
||||||
|
# await matcher.send(await UniMessage().text(err).export())
|
||||||
|
return None
|
||||||
|
assert False
|
||||||
|
|
||||||
DepImageBytes = Annotated[bytes, nonebot.params.Depends(_ext_img_data)]
|
DepImageBytes = Annotated[bytes, nonebot.params.Depends(_ext_img_data)]
|
||||||
DepPILImage = Annotated[PIL.Image.Image, nonebot.params.Depends(_ext_img)]
|
DepPILImage = Annotated[PIL.Image.Image, nonebot.params.Depends(_ext_img)]
|
||||||
|
|
||||||
DepImageBytesOrNone = Annotated[bytes | None, nonebot.params.Depends(_ext_img_data)]
|
DepImageBytesOrNone = Annotated[bytes | None, nonebot.params.Depends(_try_ext_img)]
|
||||||
|
|||||||
@ -5,8 +5,10 @@ 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"
|
||||||
|
TMP_PATH = DATA_PATH / "tmp"
|
||||||
LOG_PATH = DATA_PATH / "logs"
|
LOG_PATH = DATA_PATH / "logs"
|
||||||
CONFIG_PATH = DATA_PATH / "config"
|
CONFIG_PATH = DATA_PATH / "config"
|
||||||
|
BINARY_PATH = DATA_PATH / "bin"
|
||||||
|
|
||||||
DOCS_PATH = SRC_PATH / "docs"
|
DOCS_PATH = SRC_PATH / "docs"
|
||||||
DOCS_PATH_MAN1 = DOCS_PATH / "user"
|
DOCS_PATH_MAN1 = DOCS_PATH / "user"
|
||||||
@ -21,4 +23,6 @@ if not LOG_PATH.exists():
|
|||||||
LOG_PATH.mkdir()
|
LOG_PATH.mkdir()
|
||||||
|
|
||||||
CONFIG_PATH.mkdir(exist_ok=True)
|
CONFIG_PATH.mkdir(exist_ok=True)
|
||||||
|
TMP_PATH.mkdir(exist_ok=True)
|
||||||
|
BINARY_PATH.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
|||||||
4
konabot/docs/sys/宾几人.txt
Normal file
4
konabot/docs/sys/宾几人.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# 宾几人
|
||||||
|
|
||||||
|
查询 Bingo 有几个人。直接发送给 Bot 即可。
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ fx [滤镜名称] <参数1> <参数2> ...
|
|||||||
* ```fx 发光 <强度=0.5> <模糊半径=15>```
|
* ```fx 发光 <强度=0.5> <模糊半径=15>```
|
||||||
* ```fx 噪点 <数量=0.05>```
|
* ```fx 噪点 <数量=0.05>```
|
||||||
* ```fx 素描```
|
* ```fx 素描```
|
||||||
* ```fx 阴影 <x偏移量=10> <y偏移量=10> <模糊量=10> <不透明度=0.5> <阴影颜色=black>```
|
* ```fx 阴影 <偏移量X=10> <偏移量Y=10> <模糊量=10> <不透明度=0.5> <阴影颜色=black>```
|
||||||
|
|
||||||
### 模糊滤镜
|
### 模糊滤镜
|
||||||
* ```fx 模糊 <半径=10>```
|
* ```fx 模糊 <半径=10>```
|
||||||
@ -49,8 +49,11 @@ fx [滤镜名称] <参数1> <参数2> ...
|
|||||||
* ```fx 色彩 <因子=1.5>```
|
* ```fx 色彩 <因子=1.5>```
|
||||||
* ```fx 色调 <颜色="rgb(255,0,0)">```
|
* ```fx 色调 <颜色="rgb(255,0,0)">```
|
||||||
* ```fx RGB分离 <偏移量=5>```
|
* ```fx RGB分离 <偏移量=5>```
|
||||||
* ```fx 叠加颜色 <颜色列表=[rgb(255,0,0)|(0,0),rgb(0,255,0)|(0,100),rgb(0,0,255)|(50,100)]> <叠加模式=overlay>```
|
* ```fx 叠加颜色 <颜色列表=[rgb(255,0,0)|(0,0)+rgb(0,255,0)|(0,100)+rgb(0,0,255)|(50,100)]> <叠加模式=overlay>```
|
||||||
* ```fx 像素抖动 <最大偏移量=2>```
|
* ```fx 像素抖动 <最大偏移量=2>```
|
||||||
|
* ```fx 半调 <半径=5>```
|
||||||
|
* ```fx 描边 <半径=5> <颜色=black>```
|
||||||
|
* ```fx 形状描边 <半径=5> <颜色=black> <粗糙度=None>```
|
||||||
|
|
||||||
### 几何变换滤镜
|
### 几何变换滤镜
|
||||||
* ```fx 平移 <x偏移量=10> <y偏移量=10>```
|
* ```fx 平移 <x偏移量=10> <y偏移量=10>```
|
||||||
@ -68,11 +71,36 @@ fx [滤镜名称] <参数1> <参数2> ...
|
|||||||
* ```fx 复制 <目标位置=(100,100)> <缩放=1.0> <源区域=(0,0,100,100)>(百分比)```
|
* ```fx 复制 <目标位置=(100,100)> <缩放=1.0> <源区域=(0,0,100,100)>(百分比)```
|
||||||
|
|
||||||
### 特殊效果滤镜
|
### 特殊效果滤镜
|
||||||
|
* ```fx 设置通道 <通道=A>```
|
||||||
|
* 可用 R、G、B、A。
|
||||||
|
* ```fx 设置遮罩```
|
||||||
* ```fx 色键 <目标颜色="rgb(255,0,0)"> <容差=60>```
|
* ```fx 色键 <目标颜色="rgb(255,0,0)"> <容差=60>```
|
||||||
* ```fx 晃动 <最大偏移量=5> <运动模糊=False>```
|
* ```fx 晃动 <最大偏移量=5> <运动模糊=False>```
|
||||||
* ```fx 动图 <帧率=10>```
|
* ```fx 动图 <帧率=10>```
|
||||||
|
|
||||||
|
### 多图像处理器
|
||||||
|
* ```fx 存入图像 <目标名称>```
|
||||||
|
* 目标名称是图像的代名词,图像最长可存 12 小时,如果公用容量满了图像也会被删除。
|
||||||
|
* 该项仅可于首项使用。
|
||||||
|
* ```fx 读取图像 <目标名称>```
|
||||||
|
* 该项仅可于首项使用。
|
||||||
|
* ```fx 暂存图像```
|
||||||
|
* 此项默认插入存储在暂存列表中第一张图像的后面。
|
||||||
|
* ```fx 交换图像 <交换项=2> <交换项=1>```
|
||||||
|
* ```fx 删除图像 <删除索引=1>```
|
||||||
|
* ```fx 选择图像 <目标索引=2>```
|
||||||
|
|
||||||
|
### 多图像混合
|
||||||
|
* ```fx 混合图像 <模式=normal> <alpha=0.5>```
|
||||||
|
* ```fx 覆盖图像```
|
||||||
|
|
||||||
|
### 生成类
|
||||||
|
* ```fx 覆加颜色 <颜色列表=[rgb(255,0,0)|(0,0)+rgb(0,255,0)|(0,100)+rgb(0,0,255)|(50,100)]>```
|
||||||
|
* ```fx 生成图层 <宽度=512> <高度=512>```
|
||||||
|
* ```fx 生成文本 <文本内容=请输入文本> <字体大小=32> <文字颜色=black> <字体文件=HarmonyOS_Sans_SC_Regular.ttf>```
|
||||||
|
|
||||||
## 颜色名称支持
|
## 颜色名称支持
|
||||||
|
- **格式**:颜色列表采用 ```[颜色|位置+颜色|位置+颜色|位置]``` 的格式,位置是形如```(x百分比,y百分比)```的元组。
|
||||||
- **基本颜色**:红、绿、蓝、黄、紫、黑、白、橙、粉、灰、青、靛、棕
|
- **基本颜色**:红、绿、蓝、黄、紫、黑、白、橙、粉、灰、青、靛、棕
|
||||||
- **修饰词**:浅、深、亮、暗(可组合使用,如`浅红`、`深蓝`)
|
- **修饰词**:浅、深、亮、暗(可组合使用,如`浅红`、`深蓝`)
|
||||||
- **RGB格式**:`rgb(255,0,0)`、`rgb(0,255,0)`、`(255,0,0)` 等
|
- **RGB格式**:`rgb(255,0,0)`、`rgb(0,255,0)`、`(255,0,0)` 等
|
||||||
|
|||||||
10
konabot/docs/user/k8x12S.txt
Normal file
10
konabot/docs/user/k8x12S.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# 指令介绍
|
||||||
|
|
||||||
|
根据文字生成 k8x12S
|
||||||
|
|
||||||
|
> 「现在还不知道k8x12S是什么的可以开除界隈籍了」—— Louis, 2025/12/31
|
||||||
|
|
||||||
|
## 使用指南
|
||||||
|
|
||||||
|
`k8x12S 安心をしてください`
|
||||||
|
|
||||||
@ -1,10 +1,10 @@
|
|||||||
## 指令介绍
|
## 指令介绍
|
||||||
**`ntfy`** - 配置使用 [ntfy](https://ntfy.sh/) 来更好地为你通知此方 BOT 的代办事项。
|
**`ntfy`** - 配置使用 [ntfy](https://ntfy.sh/) 来更好地为你通知此方 BOT 的待办事项。
|
||||||
|
|
||||||
## 指令示例
|
## 指令示例
|
||||||
|
|
||||||
- **`ntfy 创建`**
|
- **`ntfy 创建`**
|
||||||
创建一个随机的 ntfy 订阅主题来提醒代办。此方 Bot 将会给你使用指引。你可以前往 [https://ntfy.sh/](https://ntfy.sh/) 官网下载 ntfy APP,或者使用网页版 ntfy。
|
创建一个随机的 ntfy 订阅主题来提醒待办。此方 Bot 将会给你使用指引。你可以前往 [https://ntfy.sh/](https://ntfy.sh/) 官网下载 ntfy APP,或者使用网页版 ntfy。
|
||||||
|
|
||||||
- **`ntfy 创建 kagami-notice`**
|
- **`ntfy 创建 kagami-notice`**
|
||||||
创建一个名称包含 `kagami-notice` 的 ntfy 订阅主题。
|
创建一个名称包含 `kagami-notice` 的 ntfy 订阅主题。
|
||||||
|
|||||||
174
konabot/docs/user/textfx.txt
Normal file
174
konabot/docs/user/textfx.txt
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
# 文字处理机器人使用手册(小白友好版)
|
||||||
|
|
||||||
|
欢迎使用文字处理机器人!你不需要懂编程,只要会打字,就能用它完成各种文字操作——比如加密、解密、打乱顺序、进制转换、排版整理等。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、基础演示
|
||||||
|
|
||||||
|
在 QQ 群里这样使用:
|
||||||
|
|
||||||
|
1. **直接输入命令**(适合短文本):
|
||||||
|
```
|
||||||
|
/textfx reverse 你好世界
|
||||||
|
```
|
||||||
|
→ 机器人回复:`界世好你`
|
||||||
|
|
||||||
|
2. **先发一段文字,再用命令处理它**(适合长文本):
|
||||||
|
- 先发送:`Hello, World!`
|
||||||
|
- 回复这条消息,输入:
|
||||||
|
```
|
||||||
|
/textfx b64 encode
|
||||||
|
```
|
||||||
|
→ 机器人返回:`SGVsbG8sIFdvcmxkIQ==`
|
||||||
|
|
||||||
|
> 命令可写为 `/textfx`、`/处理文字` 或 `/处理文本`。
|
||||||
|
> 若不回复消息,命令会处理当前行后面的文本。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、流水线语法(超简单)
|
||||||
|
|
||||||
|
- 用 `|` 连接多个操作,前一个的输出自动作为后一个的输入。
|
||||||
|
- 用 `;` 分隔多条独立指令,它们各自产生输出,最终合并显示。
|
||||||
|
- 用 `>` 或 `>>` 把结果保存起来(见下文),被重定向的指令不会产生输出。
|
||||||
|
|
||||||
|
**例子**:把"HELLO"先反转,再转成摩斯电码:(转换为摩斯电码功能暂未实现)
|
||||||
|
```
|
||||||
|
textfx reverse HELLO | morse en
|
||||||
|
```
|
||||||
|
→ 输出:`--- .-.. .-.. . ....`
|
||||||
|
|
||||||
|
**例子**:多条指令各自输出:
|
||||||
|
```
|
||||||
|
textfx echo 你好; echo 世界
|
||||||
|
```
|
||||||
|
→ 输出:
|
||||||
|
```
|
||||||
|
你好
|
||||||
|
世界
|
||||||
|
```
|
||||||
|
|
||||||
|
**例子**:重定向的指令不输出,其余正常输出:
|
||||||
|
```
|
||||||
|
textfx echo 1; echo 2 > a; echo 3
|
||||||
|
```
|
||||||
|
→ 输出:
|
||||||
|
```
|
||||||
|
1
|
||||||
|
3
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、功能清单(含示例)
|
||||||
|
|
||||||
|
### reverse(或 rev、反转)
|
||||||
|
反转文字。
|
||||||
|
示例:`/textfx reverse 爱你一万年` → `年万一你爱`
|
||||||
|
|
||||||
|
### b64(或 base64)
|
||||||
|
Base64 编码或解码。
|
||||||
|
示例:`/textfx b64 encode 你好` → `5L2g5aW9`
|
||||||
|
示例:`/textfx b64 decode 5L2g5aW9` → `你好`
|
||||||
|
|
||||||
|
### caesar(或 凯撒、rot)
|
||||||
|
凯撒密码(仅对英文字母有效)。
|
||||||
|
示例:`/textfx caesar 3 ABC` → `DEF`
|
||||||
|
示例:`/textfx caesar -3 DEF` → `ABC`
|
||||||
|
|
||||||
|
### morse(或 摩斯)
|
||||||
|
将摩斯电码解码为文字(支持英文和日文)。字符间用空格,单词间用 `/`。
|
||||||
|
示例:`/textfx morse en .... . .-.. .-.. ---` → `HELLO`
|
||||||
|
示例:`/textfx morse jp -... --.-- -.. --.. ..- ..` → `ハアホフウイ`
|
||||||
|
|
||||||
|
### baseconv(或 进制转换)
|
||||||
|
在不同进制之间转换数字。
|
||||||
|
示例:`/textfx baseconv 2 10 1101` → `13`
|
||||||
|
示例:`/textfx baseconv 10 16 255` → `FF`
|
||||||
|
|
||||||
|
### shuffle(或 打乱)
|
||||||
|
随机打乱文字顺序。
|
||||||
|
示例:`/textfx shuffle abcdef` → `fcbade`(每次结果不同)
|
||||||
|
|
||||||
|
### sort(或 排序)
|
||||||
|
将文字按字符顺序排列。
|
||||||
|
示例:`/textfx sort dbca` → `abcd`
|
||||||
|
|
||||||
|
### b64hex
|
||||||
|
在 Base64 和十六进制之间互转。
|
||||||
|
示例:`/textfx b64hex dec SGVsbG8=` → `48656c6c6f`
|
||||||
|
示例:`/textfx b64hex enc 48656c6c6f` → `SGVsbG8=`
|
||||||
|
|
||||||
|
### align(或 format、排版)
|
||||||
|
按指定格式分组排版文字。
|
||||||
|
示例:`/textfx align 2 4 0123456789abcdef` →
|
||||||
|
```
|
||||||
|
01 23 45 67
|
||||||
|
89 ab cd ef
|
||||||
|
```
|
||||||
|
|
||||||
|
### echo
|
||||||
|
输出指定文字。
|
||||||
|
示例:`/textfx echo 你好` → `你好`
|
||||||
|
|
||||||
|
### cat
|
||||||
|
读取并拼接缓存内容,类似 Unix cat 命令。
|
||||||
|
- 无参数时直接传递标准输入(管道输入或回复的消息)。
|
||||||
|
- 使用 `-` 代表标准输入,可与缓存名混合使用。
|
||||||
|
- 支持多个参数,按顺序拼接输出。
|
||||||
|
|
||||||
|
示例:
|
||||||
|
- 传递输入:`/textfx echo 你好 | cat` → `你好`
|
||||||
|
- 读取缓存:`/textfx cat mytext` → 输出 mytext 的内容
|
||||||
|
- 拼接多个缓存:`/textfx cat a b c` → 依次拼接缓存 a、b、c
|
||||||
|
- 混合标准输入和缓存:`/textfx echo 前缀 | cat - mytext` → 拼接标准输入与缓存 mytext
|
||||||
|
|
||||||
|
### 缓存操作(保存中间结果)
|
||||||
|
- 保存:`/textfx reverse 你好 > mytext`(不输出,存入 mytext)
|
||||||
|
- 读取:`/textfx cat mytext` → `好你`
|
||||||
|
- 追加:`/textfx echo world >> mytext`
|
||||||
|
- 删除:`/textfx rm mytext`
|
||||||
|
|
||||||
|
> 缓存仅在当前对话中有效,重启后清空。
|
||||||
|
|
||||||
|
### replace(或 替换、sed)
|
||||||
|
替换文字(支持正则表达式)。
|
||||||
|
示例(普通):`/textfx replace 世界 宇宙 你好世界` → `你好宇宙`
|
||||||
|
示例(正则):`/textfx replace \d+ [数字] 我有123个苹果` → `我有[数字]个苹果`
|
||||||
|
|
||||||
|
### trim(或 strip、去空格)
|
||||||
|
去除文本首尾空白字符。
|
||||||
|
示例:`/textfx trim " 你好 "` → `你好`
|
||||||
|
示例:`/textfx echo " hello " | trim` → `hello`
|
||||||
|
|
||||||
|
### ltrim(或 lstrip)
|
||||||
|
去除文本左侧空白字符。
|
||||||
|
示例:`/textfx ltrim " 你好 "` → `你好 `
|
||||||
|
|
||||||
|
### rtrim(或 rstrip)
|
||||||
|
去除文本右侧空白字符。
|
||||||
|
示例:`/textfx rtrim " 你好 "` → ` 你好`
|
||||||
|
|
||||||
|
### squeeze(或 压缩空白)
|
||||||
|
将连续的空白字符(空格、制表符)压缩为单个空格。
|
||||||
|
示例:`/textfx squeeze "你好 世界"` → `你好 世界`
|
||||||
|
|
||||||
|
### lines(或 行处理)
|
||||||
|
按行处理文本,支持以下子命令:
|
||||||
|
- `lines trim` — 去除每行首尾空白
|
||||||
|
- `lines empty` — 去除所有空行
|
||||||
|
- `lines squeeze` — 将连续空行压缩为一行
|
||||||
|
|
||||||
|
示例:`/textfx echo " hello\n\n\n world " | lines trim` → `hello\n\n\n world`
|
||||||
|
示例:`/textfx echo "a\n\n\nb" | lines squeeze` → `a\n\nb`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
- **没反应?** 可能内容被安全系统拦截,机器人会提示“内容被拦截”。
|
||||||
|
- **只支持纯文字**,暂不支持图片或文件。
|
||||||
|
- 命令拼错时,机器人会提示“不存在名为 xxx 的函数”,请检查名称。
|
||||||
|
|
||||||
|
快去试试吧!用法核心:**`/textfx` + 你的操作**
|
||||||
4
konabot/docs/user/typst.txt
Normal file
4
konabot/docs/user/typst.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Typst 渲染
|
||||||
|
|
||||||
|
只需使用 `typst ...` 就可以渲染 Typst 了
|
||||||
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
## 指令介绍
|
|
||||||
|
|
||||||
**黑白** - 将图片经过一个黑白滤镜的处理
|
|
||||||
|
|
||||||
## 示例
|
|
||||||
|
|
||||||
引用一个带有图片的消息,或者消息本身携带图片,然后发送「黑白」即可
|
|
||||||
52
konabot/plugins/ai_extract_text/__init__.py
Normal file
52
konabot/plugins/ai_extract_text/__init__.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
from io import BytesIO
|
||||||
|
import base64
|
||||||
|
import re
|
||||||
|
from loguru import logger
|
||||||
|
from nonebot import on_message
|
||||||
|
from nonebot.rule import Rule
|
||||||
|
|
||||||
|
from konabot.common.apis.ali_content_safety import AlibabaGreen
|
||||||
|
from konabot.common.llm import get_llm
|
||||||
|
from konabot.common.longtask import DepLongTaskTarget
|
||||||
|
from konabot.common.nb.extract_image import DepPILImage
|
||||||
|
from konabot.common.nb.match_keyword import match_keyword
|
||||||
|
|
||||||
|
|
||||||
|
cmd = on_message(rule=Rule(match_keyword(re.compile(r"^千问识图\s*$"))))
|
||||||
|
|
||||||
|
|
||||||
|
@cmd.handle()
|
||||||
|
async def _(img: DepPILImage, target: DepLongTaskTarget):
|
||||||
|
if 1:
|
||||||
|
return #TODO:这里还没写完,还有 Bug 要修
|
||||||
|
jpeg_data = BytesIO()
|
||||||
|
if img.width > 2160:
|
||||||
|
img = img.resize((2160, img.height * 2160 // img.width))
|
||||||
|
if img.height > 2160:
|
||||||
|
img = img.resize((img.width * 2160 // img.height, 2160))
|
||||||
|
img = img.convert("RGB")
|
||||||
|
img.save(jpeg_data, format="jpeg", optimize=True, quality=85)
|
||||||
|
data_url = "data:image/jpeg;base64,"
|
||||||
|
data_url += base64.b64encode(jpeg_data.getvalue()).decode('ascii')
|
||||||
|
|
||||||
|
llm = get_llm("qwen3-vl-plus")
|
||||||
|
res = await llm.chat([
|
||||||
|
{ "role": "user", "content": [
|
||||||
|
{ "type": "image_url", "image_url": {
|
||||||
|
"url": data_url
|
||||||
|
} },
|
||||||
|
{ "type": "text", "text": "请你提取这张图片中的所有文字,并尽量按照原图的排版输出,不需要其他内容" },
|
||||||
|
] }
|
||||||
|
])
|
||||||
|
result = res.content
|
||||||
|
logger.info(res)
|
||||||
|
if result is None:
|
||||||
|
await target.send_message("提取失败:可能存在网络异常")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not await AlibabaGreen.detect(result):
|
||||||
|
await target.send_message("提取失败:图片中可能存在一些不合适的内容")
|
||||||
|
return
|
||||||
|
|
||||||
|
await target.send_message(result, at=False)
|
||||||
|
|
||||||
@ -1,18 +1,29 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from nonebot import on_message
|
from nonebot import get_plugin_config, on_message
|
||||||
from nonebot_plugin_alconna import Reference, Reply, UniMsg
|
from nonebot_plugin_alconna import Reference, Reply, UniMsg
|
||||||
|
|
||||||
from nonebot.adapters import Event
|
from nonebot.adapters import Event
|
||||||
|
from nonebot.adapters.onebot.v11.event import GroupMessageEvent as OB11GroupEvent
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Config(BaseModel):
|
||||||
|
bilifetch_enabled_groups: list[int] = []
|
||||||
|
|
||||||
|
|
||||||
|
config = get_plugin_config(Config)
|
||||||
pattern = (
|
pattern = (
|
||||||
r"^(?:(?:av|cv)\d+|BV[a-zA-Z0-9]{10})|"
|
r"^(?:(?:av|cv)\d+|BV[a-zA-Z0-9]{10})|"
|
||||||
r"(?:b23\.tv|bili(?:22|23|33|2233)\.cn|\.bilibili\.com|QQ小程序(?:&#93;|]|\])哔哩哔哩).{0,500}"
|
r"(?:b23\.tv|bili(?:22|23|33|2233)\.cn|\.bilibili\.com|QQ小程序(?:&#93;|]|\])哔哩哔哩).{0,500}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _rule(msg: UniMsg):
|
def _rule(msg: UniMsg, evt: Event) -> bool:
|
||||||
|
if isinstance(evt, OB11GroupEvent):
|
||||||
|
if evt.group_id not in config.bilifetch_enabled_groups:
|
||||||
|
return False
|
||||||
|
|
||||||
to_search = msg.exclude(Reply, Reference).dump(json=True)
|
to_search = msg.exclude(Reply, Reference).dump(json=True)
|
||||||
to_search2 = msg.exclude(Reply, Reference).extract_plain_text()
|
to_search2 = msg.exclude(Reply, Reference).extract_plain_text()
|
||||||
if not re.search(pattern, to_search) and not re.search(pattern, to_search2):
|
if not re.search(pattern, to_search) and not re.search(pattern, to_search2):
|
||||||
|
|||||||
132
konabot/plugins/celeste_classic/__init__.py
Normal file
132
konabot/plugins/celeste_classic/__init__.py
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from loguru import logger
|
||||||
|
from nonebot import on_command
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from nonebot.adapters import Event, Bot
|
||||||
|
from nonebot_plugin_alconna import UniMessage, UniMsg
|
||||||
|
from nonebot.adapters.onebot.v11.event import MessageEvent as OB11MessageEvent
|
||||||
|
|
||||||
|
from konabot.common.artifact import ArtifactDepends, ensure_artifact, register_artifacts
|
||||||
|
from konabot.common.data_man import DataManager
|
||||||
|
from konabot.common.path import BINARY_PATH, DATA_PATH
|
||||||
|
|
||||||
|
|
||||||
|
arti_ccleste_wrap_linux = ArtifactDepends(
|
||||||
|
url="https://github.com/Passthem-desu/pt-ccleste-wrap/releases/download/v0.1.5/ccleste-wrap",
|
||||||
|
sha256="ba4118c6465d1ca1547cdd1bd11c6b9e6a6a98ea8967b55485aeb6b77bb7e921",
|
||||||
|
target=BINARY_PATH / "ccleste-wrap",
|
||||||
|
required_os="Linux",
|
||||||
|
required_arch="x86_64",
|
||||||
|
)
|
||||||
|
arti_ccleste_wrap_windows = ArtifactDepends(
|
||||||
|
url="https://github.com/Passthem-desu/pt-ccleste-wrap/releases/download/v0.1.5/ccleste-wrap.exe",
|
||||||
|
sha256="7df382486a452485cdcf2115eabd7f772339ece470ab344074dc163fc7981feb",
|
||||||
|
target=BINARY_PATH / "ccleste-wrap.exe",
|
||||||
|
required_os="Windows",
|
||||||
|
required_arch="AMD64",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
register_artifacts(arti_ccleste_wrap_linux)
|
||||||
|
register_artifacts(arti_ccleste_wrap_windows)
|
||||||
|
|
||||||
|
|
||||||
|
class CelesteStatus(BaseModel):
|
||||||
|
records: dict[str, str] = {}
|
||||||
|
|
||||||
|
|
||||||
|
celeste_status = DataManager(CelesteStatus, DATA_PATH / "celeste-status.json")
|
||||||
|
|
||||||
|
|
||||||
|
cmd = on_command(cmd="celeste", aliases={"蔚蓝", "爬山", "鳌太线"})
|
||||||
|
|
||||||
|
|
||||||
|
@cmd.handle()
|
||||||
|
async def _(msg: UniMsg, evt: Event, bot: Bot):
|
||||||
|
prev = None
|
||||||
|
if isinstance(evt, OB11MessageEvent):
|
||||||
|
if evt.reply is not None:
|
||||||
|
prev = f"QQ:{bot.self_id}:" + str(evt.reply.message_id)
|
||||||
|
else:
|
||||||
|
for seg in evt.get_message():
|
||||||
|
if seg.type == 'reply':
|
||||||
|
msgid = seg.get('id')
|
||||||
|
prev = f"QQ:{bot.self_id}:" + str(msgid)
|
||||||
|
|
||||||
|
actions = msg.extract_plain_text().strip().removeprefix("celeste")
|
||||||
|
for alias in {"蔚蓝", "爬山", "鳌太线"}:
|
||||||
|
actions = actions.removeprefix(alias)
|
||||||
|
actions = actions.strip()
|
||||||
|
if len(actions) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
if prev is not None:
|
||||||
|
async with celeste_status.get_data() as data:
|
||||||
|
prev = data.records.get(prev)
|
||||||
|
|
||||||
|
await ensure_artifact(arti_ccleste_wrap_linux)
|
||||||
|
await ensure_artifact(arti_ccleste_wrap_windows)
|
||||||
|
|
||||||
|
bin: Path | None = None
|
||||||
|
for arti in (
|
||||||
|
arti_ccleste_wrap_linux,
|
||||||
|
arti_ccleste_wrap_windows,
|
||||||
|
):
|
||||||
|
if not arti.is_corresponding_platform():
|
||||||
|
continue
|
||||||
|
bin = arti.target
|
||||||
|
if not bin.exists():
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
|
||||||
|
if bin is None:
|
||||||
|
logger.warning("Celeste 模块没有找到该系统需要的二进制文件")
|
||||||
|
return
|
||||||
|
|
||||||
|
if prev is not None:
|
||||||
|
prev_append = ["-p", prev]
|
||||||
|
else:
|
||||||
|
prev_append = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
with tempfile.TemporaryDirectory() as _tempdir:
|
||||||
|
tempdir = Path(_tempdir)
|
||||||
|
gif_path = tempdir / "render.gif"
|
||||||
|
cmd_celeste = [
|
||||||
|
bin,
|
||||||
|
"-a",
|
||||||
|
actions,
|
||||||
|
"-o",
|
||||||
|
gif_path,
|
||||||
|
] + prev_append
|
||||||
|
logger.info(f"执行指令调用 celeste: CMD={cmd_celeste}")
|
||||||
|
res = subprocess.run(cmd_celeste, timeout=5, capture_output=True)
|
||||||
|
if res.returncode != 0:
|
||||||
|
logger.warning(f"渲染 Celeste 时的输出不是 0 CODE={res.returncode} STDOUT={res.stdout} STDERR={res.stderr}")
|
||||||
|
await UniMessage.text(f"渲染 Celeste 时出错啦!下面是输出:\n\n{res.stdout.decode()}{res.stderr.decode()}").send(evt, bot, at_sender=True)
|
||||||
|
return
|
||||||
|
if not gif_path.exists():
|
||||||
|
logger.warning("没有找到 Celeste 渲染的文件")
|
||||||
|
await UniMessage.text("渲染 Celeste 时出错啦!").send(evt, bot, at_sender=True)
|
||||||
|
return
|
||||||
|
gif_data = gif_path.read_bytes()
|
||||||
|
except TimeoutError:
|
||||||
|
logger.warning("在渲染 Celeste 时超时了")
|
||||||
|
await UniMessage("渲染 Celeste 时超时了!请检查你的操作清单,不能太长").send(evt, bot, at_sender=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
receipt = await UniMessage.image(raw=gif_data).send(evt, bot)
|
||||||
|
async with celeste_status.get_data() as data:
|
||||||
|
if prev:
|
||||||
|
actions = prev + "\n" + actions
|
||||||
|
if isinstance(evt, OB11MessageEvent):
|
||||||
|
for _msgid in receipt.msg_ids:
|
||||||
|
msgid = _msgid["message_id"]
|
||||||
|
data.records[f"QQ:{bot.self_id}:{msgid}"] = actions
|
||||||
|
else:
|
||||||
|
for msgid in receipt.msg_ids:
|
||||||
|
data.records[f"DISCORD:{bot.self_id}:{msgid}"] = actions
|
||||||
|
|
||||||
@ -1,25 +1,54 @@
|
|||||||
import asyncio as asynkio
|
import asyncio as asynkio
|
||||||
from dataclasses import dataclass
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from inspect import signature
|
from inspect import signature
|
||||||
|
import random
|
||||||
|
|
||||||
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 DepImageBytesOrNone
|
from konabot.common.nb.extract_image import DepImageBytesOrNone
|
||||||
from nonebot.adapters import Event as BaseEvent
|
from nonebot.adapters import Event as BaseEvent
|
||||||
from nonebot import on_message, logger
|
from nonebot import on_message, logger
|
||||||
from returns.result import Failure, Result, Success
|
|
||||||
|
|
||||||
from nonebot_plugin_alconna import (
|
from nonebot_plugin_alconna import (
|
||||||
UniMessage,
|
UniMessage,
|
||||||
UniMsg
|
UniMsg
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from konabot.plugins.fx_process.fx_handle import ImageFilterStorage
|
||||||
from konabot.plugins.fx_process.fx_manager import ImageFilterManager
|
from konabot.plugins.fx_process.fx_manager import ImageFilterManager
|
||||||
|
|
||||||
from PIL import Image, ImageSequence
|
from PIL import Image, ImageSequence
|
||||||
|
|
||||||
from konabot.plugins.fx_process.types import FilterItem, ImageRequireSignal, ImagesListRequireSignal, SenderInfo
|
from konabot.plugins.fx_process.types import FilterItem, ImageRequireSignal, ImagesListRequireSignal, SenderInfo, StoredInfo
|
||||||
|
|
||||||
|
def try_convert_type(param_type, input_param, sender_info: SenderInfo = None) -> tuple[bool, any]:
|
||||||
|
converted_value = None
|
||||||
|
try:
|
||||||
|
if param_type is float:
|
||||||
|
converted_value = float(input_param)
|
||||||
|
elif param_type is int:
|
||||||
|
converted_value = int(input_param)
|
||||||
|
elif param_type is bool:
|
||||||
|
converted_value = input_param.lower() in ['true', '1', 'yes', '是', '开']
|
||||||
|
elif param_type is Image.Image:
|
||||||
|
converted_value = ImageRequireSignal()
|
||||||
|
return False, converted_value
|
||||||
|
elif param_type is SenderInfo:
|
||||||
|
converted_value = sender_info
|
||||||
|
return False, converted_value
|
||||||
|
elif param_type == list[Image.Image]:
|
||||||
|
converted_value = ImagesListRequireSignal()
|
||||||
|
return False, converted_value
|
||||||
|
elif param_type is str:
|
||||||
|
if input_param is None:
|
||||||
|
return False, None
|
||||||
|
converted_value = str(input_param)
|
||||||
|
else:
|
||||||
|
return False, None
|
||||||
|
except Exception:
|
||||||
|
return False, None
|
||||||
|
return True, converted_value
|
||||||
|
|
||||||
def prase_input_args(input_str: str, sender_info: SenderInfo = None) -> list[FilterItem]:
|
def prase_input_args(input_str: str, sender_info: SenderInfo = None) -> list[FilterItem]:
|
||||||
# 按分号或换行符分割参数
|
# 按分号或换行符分割参数
|
||||||
@ -39,34 +68,24 @@ def prase_input_args(input_str: str, sender_info: SenderInfo = None) -> list[Fil
|
|||||||
max_params = len(sig.parameters) - 1 # 减去第一个参数 image
|
max_params = len(sig.parameters) - 1 # 减去第一个参数 image
|
||||||
# 从 args 提取参数,并转换为适当类型
|
# 从 args 提取参数,并转换为适当类型
|
||||||
func_args = []
|
func_args = []
|
||||||
for i in range(0, min(len(input_filter_args), max_params)):
|
for i in range(0, max_params):
|
||||||
# 尝试将参数转换为函数签名中对应的类型,并检测是不是 Image 类型,如果有则表示多个图像输入
|
# 尝试将参数转换为函数签名中对应的类型
|
||||||
param = list(sig.parameters.values())[i + 1]
|
param = list(sig.parameters.values())[i + 1]
|
||||||
param_type = param.annotation
|
param_type = param.annotation
|
||||||
arg_value = input_filter_args[i]
|
# 根据函数所需要的参数,从输入参数中提取,如果不匹配就使用默认值,将当前参数递交给下一个循环
|
||||||
try:
|
input_param = input_filter_args[0] if len(input_filter_args) > 0 else None
|
||||||
if param_type is float:
|
state, converted_param = try_convert_type(param_type, input_param, sender_info)
|
||||||
converted_value = float(arg_value)
|
if state:
|
||||||
elif param_type is int:
|
input_filter_args.pop(0)
|
||||||
converted_value = int(arg_value)
|
if converted_param is None and param.default != param.empty:
|
||||||
elif param_type is bool:
|
converted_param = param.default
|
||||||
converted_value = arg_value.lower() in ['true', '1', 'yes', '是', '开']
|
func_args.append(converted_param)
|
||||||
elif param_type is Image.Image:
|
|
||||||
converted_value = ImageRequireSignal()
|
|
||||||
elif param_type is SenderInfo:
|
|
||||||
converted_value = sender_info
|
|
||||||
elif param_type is list[Image.Image]:
|
|
||||||
converted_value = ImagesListRequireSignal()
|
|
||||||
else:
|
|
||||||
converted_value = arg_value
|
|
||||||
except Exception:
|
|
||||||
converted_value = arg_value
|
|
||||||
func_args.append(converted_value)
|
|
||||||
args.append(FilterItem(name=filter_name,filter=filter_func, args=func_args))
|
args.append(FilterItem(name=filter_name,filter=filter_func, args=func_args))
|
||||||
return args
|
return args
|
||||||
|
|
||||||
def handle_filters_to_image(images: list[Image.Image], filters: list[FilterItem]) -> Image.Image:
|
def handle_filters_to_image(images: list[Image.Image], filters: list[FilterItem]) -> Image.Image:
|
||||||
for filter_item in filters:
|
for filter_item in filters:
|
||||||
|
logger.debug(f"{filter_item}")
|
||||||
filter_func = filter_item.filter
|
filter_func = filter_item.filter
|
||||||
func_args = filter_item.args
|
func_args = filter_item.args
|
||||||
# 检测参数中是否有 ImageRequireSignal,如果有则传入对应数量的图像列表
|
# 检测参数中是否有 ImageRequireSignal,如果有则传入对应数量的图像列表
|
||||||
@ -76,6 +95,8 @@ def handle_filters_to_image(images: list[Image.Image], filters: list[FilterItem]
|
|||||||
img_signal_count = 1 # 从 images[1] 开始取图像
|
img_signal_count = 1 # 从 images[1] 开始取图像
|
||||||
for arg in func_args:
|
for arg in func_args:
|
||||||
if isinstance(arg, ImageRequireSignal):
|
if isinstance(arg, ImageRequireSignal):
|
||||||
|
if img_signal_count >= len(images):
|
||||||
|
raise BotExceptionMessage("图像数量不足,无法满足滤镜需求!")
|
||||||
actual_args.append(images[img_signal_count])
|
actual_args.append(images[img_signal_count])
|
||||||
img_signal_count += 1
|
img_signal_count += 1
|
||||||
else:
|
else:
|
||||||
@ -90,21 +111,62 @@ def handle_filters_to_image(images: list[Image.Image], filters: list[FilterItem]
|
|||||||
else:
|
else:
|
||||||
actual_args.append(arg)
|
actual_args.append(arg)
|
||||||
func_args = actual_args
|
func_args = actual_args
|
||||||
|
|
||||||
|
logger.debug(f"Applying filter: {filter_item.name} with args: {func_args}")
|
||||||
|
|
||||||
images[0] = filter_func(images[0], *func_args)
|
images[0] = filter_func(images[0], *func_args)
|
||||||
return images[0]
|
return images[0]
|
||||||
|
|
||||||
async def apply_filters_to_images(images: list[Image.Image], filters: list[FilterItem]) -> BytesIO:
|
def copy_images_by_index(images: list[Image.Image], index: int) -> list[Image.Image]:
|
||||||
# 如果第一项是“加载图像”参数,那么就加载图像
|
# 将导入图像列表复制为新的图像列表,如果是动图,那么就找到对应索引下的帧
|
||||||
if filters and filters[0].name == "加载图像":
|
new_images = []
|
||||||
load_filter = filters.pop(0)
|
for img in images:
|
||||||
# 加载全部路径
|
if getattr(img, "is_animated", False):
|
||||||
for path in load_filter.args:
|
frames = img.n_frames
|
||||||
img = Image.open(path)
|
frame_idx = index % frames
|
||||||
images.append(img)
|
img.seek(frame_idx)
|
||||||
|
new_images.append(img.copy())
|
||||||
|
else:
|
||||||
|
new_images.append(img.copy())
|
||||||
|
|
||||||
|
return new_images
|
||||||
|
|
||||||
|
def generate_image(images: list[Image.Image], filters: list[FilterItem]) -> Image.Image:
|
||||||
|
# 处理位于最前面的生成类滤镜
|
||||||
|
while filters and filters[0].name.strip() in ImageFilterManager.generate_filter_map:
|
||||||
|
gen_filter = filters.pop(0)
|
||||||
|
gen_func = gen_filter.filter
|
||||||
|
func_args = gen_filter.args[1:] # 去掉第一个 list 参数
|
||||||
|
gen_func(None, images, *func_args)
|
||||||
|
|
||||||
|
def save_or_load_image(images: list[Image.Image], filters: list[FilterItem], sender_info: SenderInfo) -> StoredInfo | None:
|
||||||
|
stored_info = None
|
||||||
|
# 处理位于最前面的“读取图像”、“存入图像”
|
||||||
|
if not filters:
|
||||||
|
return
|
||||||
|
while filters and filters[0].name.strip() in ["读取图像", "存入图像"]:
|
||||||
|
if filters[0].name.strip() == "读取图像":
|
||||||
|
load_filter = filters.pop(0)
|
||||||
|
path = load_filter.args[0] if load_filter.args else ""
|
||||||
|
ImageFilterStorage.load_image(None, path, images, sender_info)
|
||||||
|
elif filters[0].name.strip() == "存入图像":
|
||||||
|
store_filter = filters.pop(0)
|
||||||
|
name = store_filter.args[0] if store_filter.args[0] else str(random.randint(10000,99999))
|
||||||
|
stored_info = ImageFilterStorage.store_image(images[0], name, sender_info)
|
||||||
|
# 将剩下的“读取图像”或“存入图像”参数全部删除,避免后续非法操作
|
||||||
|
filters[:] = [f for f in filters if f.name.strip() not in ["读取图像", "存入图像"]]
|
||||||
|
return stored_info
|
||||||
|
|
||||||
|
async def apply_filters_to_images(images: list[Image.Image], filters: list[FilterItem], sender_info: SenderInfo) -> BytesIO | StoredInfo:
|
||||||
|
# 先处理存取图像、生成图像的操作
|
||||||
|
stored_info = save_or_load_image(images, filters, sender_info)
|
||||||
|
generate_image(images, filters)
|
||||||
|
|
||||||
|
if stored_info and len(filters) <= 0:
|
||||||
|
return stored_info
|
||||||
|
|
||||||
if len(images) <= 0:
|
if len(images) <= 0:
|
||||||
raise ValueError("没有提供任何图像进行处理")
|
raise BotExceptionMessage("没有可处理的图像!")
|
||||||
|
|
||||||
# 检测是否需要将静态图视作动图处理
|
# 检测是否需要将静态图视作动图处理
|
||||||
frozen_to_move = any(
|
frozen_to_move = any(
|
||||||
@ -127,32 +189,32 @@ async def apply_filters_to_images(images: list[Image.Image], filters: list[Filte
|
|||||||
output = BytesIO()
|
output = BytesIO()
|
||||||
if getattr(img, "is_animated", False) or frozen_to_move:
|
if getattr(img, "is_animated", False) or frozen_to_move:
|
||||||
frames = []
|
frames = []
|
||||||
all_frames = []
|
append_images = []
|
||||||
if getattr(img, "is_animated", False):
|
if getattr(img, "is_animated", False):
|
||||||
logger.debug("处理动图帧")
|
logger.debug("处理动图帧")
|
||||||
for frame in ImageSequence.Iterator(img):
|
|
||||||
frame_copy = frame.copy()
|
|
||||||
all_frames.append(frame_copy)
|
|
||||||
else:
|
else:
|
||||||
# 将静态图视作单帧动图处理,拷贝多份
|
# 将静态图视作单帧动图处理,拷贝 10 帧
|
||||||
logger.debug("处理静态图为多帧动图")
|
logger.debug("处理静态图为多帧动图")
|
||||||
for _ in range(10): # 默认复制10帧
|
append_images = [img.copy() for _ in range(10)]
|
||||||
all_frames.append(img.copy())
|
|
||||||
img.info['duration'] = int(1000 / static_fps)
|
img.info['duration'] = int(1000 / static_fps)
|
||||||
|
|
||||||
async def process_single_frame(frame: list[Image.Image], frame_idx: int) -> Image.Image:
|
async def process_single_frame(frame_images: list[Image.Image], frame_idx: int) -> Image.Image:
|
||||||
"""处理单帧的异步函数"""
|
"""处理单帧的异步函数"""
|
||||||
logger.debug(f"开始处理帧 {frame_idx}")
|
logger.debug(f"开始处理帧 {frame_idx}")
|
||||||
result = await asynkio.to_thread(handle_filters_to_image, frame, images, filters)
|
result = await asynkio.to_thread(handle_filters_to_image, frame_images, filters)
|
||||||
logger.debug(f"完成处理帧 {frame_idx}")
|
logger.debug(f"完成处理帧 {frame_idx}")
|
||||||
return result[0]
|
return result
|
||||||
|
|
||||||
# 并发处理所有帧
|
# 并发处理所有帧
|
||||||
tasks = []
|
tasks = []
|
||||||
for i, frame in enumerate(all_frames):
|
all_frames = []
|
||||||
task = process_single_frame(frame, i)
|
for i, frame in enumerate(list(ImageSequence.Iterator(img)) + append_images):
|
||||||
|
all_frames.append(frame.copy())
|
||||||
|
images_copy = copy_images_by_index(images, i)
|
||||||
|
task = process_single_frame(images_copy, i)
|
||||||
tasks.append(task)
|
tasks.append(task)
|
||||||
|
|
||||||
frames = await asynkio.gather(*tasks, return_exceptions=True)
|
frames = await asynkio.gather(*tasks, return_exceptions=False)
|
||||||
|
|
||||||
# 检查是否有处理失败的帧
|
# 检查是否有处理失败的帧
|
||||||
for i, result in enumerate(frames):
|
for i, result in enumerate(frames):
|
||||||
@ -188,13 +250,11 @@ def is_fx_mentioned(evt: BaseEvent, msg: UniMsg) -> bool:
|
|||||||
fx_on = on_message(rule=is_fx_mentioned)
|
fx_on = on_message(rule=is_fx_mentioned)
|
||||||
|
|
||||||
@fx_on.handle()
|
@fx_on.handle()
|
||||||
async def _(msg: UniMsg, event: BaseEvent, target: DepLongTaskTarget, image_data: DepImageBytesOrNone = None):
|
async def _(msg: UniMsg, event: BaseEvent, target: DepLongTaskTarget, image_data: DepImageBytesOrNone):
|
||||||
preload_imgs = []
|
preload_imgs = []
|
||||||
# 提取图像
|
# 提取图像
|
||||||
try:
|
try:
|
||||||
if image_data is not None:
|
preload_imgs.append(Image.open(BytesIO(image_data)))
|
||||||
preload_imgs.append(Image.open(BytesIO(image_data)))
|
|
||||||
logger.debug("Image extracted for FX processing.")
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.info("No image found in message for FX processing.")
|
logger.info("No image found in message for FX processing.")
|
||||||
args = msg.extract_plain_text().split()
|
args = msg.extract_plain_text().split()
|
||||||
@ -207,9 +267,11 @@ async def _(msg: UniMsg, event: BaseEvent, target: DepLongTaskTarget, image_data
|
|||||||
)
|
)
|
||||||
|
|
||||||
filters = prase_input_args(msg.extract_plain_text()[2:], sender_info=sender_info)
|
filters = prase_input_args(msg.extract_plain_text()[2:], sender_info=sender_info)
|
||||||
if not filters:
|
# if not filters:
|
||||||
return
|
# return
|
||||||
output = await apply_filters_to_images(preload_imgs, filters)
|
output = await apply_filters_to_images(preload_imgs, filters, sender_info)
|
||||||
logger.debug("FX processing completed, sending result.")
|
if isinstance(output,StoredInfo):
|
||||||
await fx_on.send(await UniMessage().image(raw=output).export())
|
await fx_on.send(await UniMessage().text(f"图像已存为「{output.name}」!").export())
|
||||||
|
elif isinstance(output,BytesIO):
|
||||||
|
await fx_on.send(await UniMessage().image(raw=output).export())
|
||||||
|
|
||||||
@ -1,9 +1,12 @@
|
|||||||
import random
|
import random
|
||||||
from PIL import Image, ImageFilter
|
from PIL import Image, ImageFilter, ImageDraw, ImageStat, ImageFont
|
||||||
from PIL import ImageEnhance
|
from PIL import ImageEnhance
|
||||||
from PIL import ImageChops
|
from PIL import ImageChops
|
||||||
from PIL import ImageOps
|
from PIL import ImageOps
|
||||||
|
import cv2
|
||||||
|
from nonebot import logger
|
||||||
|
|
||||||
|
from konabot.common.path import FONTS_PATH
|
||||||
from konabot.plugins.fx_process.color_handle import ColorHandle
|
from konabot.plugins.fx_process.color_handle import ColorHandle
|
||||||
|
|
||||||
import math
|
import math
|
||||||
@ -12,16 +15,23 @@ from konabot.plugins.fx_process.gradient import GradientGenerator
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from konabot.plugins.fx_process.image_storage import ImageStorager
|
from konabot.plugins.fx_process.image_storage import ImageStorager
|
||||||
from konabot.plugins.fx_process.types import SenderInfo
|
from konabot.plugins.fx_process.math_helper import expand_contours
|
||||||
|
from konabot.plugins.fx_process.types import SenderInfo, StoredInfo
|
||||||
|
|
||||||
class ImageFilterImplement:
|
class ImageFilterImplement:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def apply_blur(image: Image.Image, radius: float = 10) -> Image.Image:
|
def apply_blur(image: Image.Image, radius: float = 10) -> Image.Image:
|
||||||
|
if image.mode != 'RGBA':
|
||||||
|
image = image.convert('RGBA')
|
||||||
|
|
||||||
return image.filter(ImageFilter.GaussianBlur(radius))
|
return image.filter(ImageFilter.GaussianBlur(radius))
|
||||||
|
|
||||||
# 马赛克
|
# 马赛克
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def apply_mosaic(image: Image.Image, pixel_size: int = 10) -> Image.Image:
|
def apply_mosaic(image: Image.Image, pixel_size: int = 10) -> Image.Image:
|
||||||
|
if image.mode != 'RGBA':
|
||||||
|
image = image.convert('RGBA')
|
||||||
|
|
||||||
if pixel_size <= 0:
|
if pixel_size <= 0:
|
||||||
pixel_size = 1
|
pixel_size = 1
|
||||||
# 缩小图像
|
# 缩小图像
|
||||||
@ -34,26 +44,38 @@ class ImageFilterImplement:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def apply_contour(image: Image.Image) -> Image.Image:
|
def apply_contour(image: Image.Image) -> Image.Image:
|
||||||
|
if image.mode != 'RGBA':
|
||||||
|
image = image.convert('RGBA')
|
||||||
return image.filter(ImageFilter.CONTOUR)
|
return image.filter(ImageFilter.CONTOUR)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def apply_sharpen(image: Image.Image) -> Image.Image:
|
def apply_sharpen(image: Image.Image) -> Image.Image:
|
||||||
|
if image.mode != 'RGBA':
|
||||||
|
image = image.convert('RGBA')
|
||||||
return image.filter(ImageFilter.SHARPEN)
|
return image.filter(ImageFilter.SHARPEN)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def apply_edge_enhance(image: Image.Image) -> Image.Image:
|
def apply_edge_enhance(image: Image.Image) -> Image.Image:
|
||||||
|
if image.mode != 'RGBA':
|
||||||
|
image = image.convert('RGBA')
|
||||||
return image.filter(ImageFilter.EDGE_ENHANCE)
|
return image.filter(ImageFilter.EDGE_ENHANCE)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def apply_emboss(image: Image.Image) -> Image.Image:
|
def apply_emboss(image: Image.Image) -> Image.Image:
|
||||||
|
if image.mode != 'RGBA':
|
||||||
|
image = image.convert('RGBA')
|
||||||
return image.filter(ImageFilter.EMBOSS)
|
return image.filter(ImageFilter.EMBOSS)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def apply_find_edges(image: Image.Image) -> Image.Image:
|
def apply_find_edges(image: Image.Image) -> Image.Image:
|
||||||
|
if image.mode != 'RGBA':
|
||||||
|
image = image.convert('RGBA')
|
||||||
return image.filter(ImageFilter.FIND_EDGES)
|
return image.filter(ImageFilter.FIND_EDGES)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def apply_smooth(image: Image.Image) -> Image.Image:
|
def apply_smooth(image: Image.Image) -> Image.Image:
|
||||||
|
if image.mode != 'RGBA':
|
||||||
|
image = image.convert('RGBA')
|
||||||
return image.filter(ImageFilter.SMOOTH)
|
return image.filter(ImageFilter.SMOOTH)
|
||||||
|
|
||||||
# 反色
|
# 反色
|
||||||
@ -94,18 +116,24 @@ class ImageFilterImplement:
|
|||||||
# 对比度
|
# 对比度
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def apply_contrast(image: Image.Image, factor: float = 1.5) -> Image.Image:
|
def apply_contrast(image: Image.Image, factor: float = 1.5) -> Image.Image:
|
||||||
|
if image.mode != 'RGBA':
|
||||||
|
image = image.convert('RGBA')
|
||||||
enhancer = ImageEnhance.Contrast(image)
|
enhancer = ImageEnhance.Contrast(image)
|
||||||
return enhancer.enhance(factor)
|
return enhancer.enhance(factor)
|
||||||
|
|
||||||
# 亮度
|
# 亮度
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def apply_brightness(image: Image.Image, factor: float = 1.5) -> Image.Image:
|
def apply_brightness(image: Image.Image, factor: float = 1.5) -> Image.Image:
|
||||||
|
if image.mode != 'RGBA':
|
||||||
|
image = image.convert('RGBA')
|
||||||
enhancer = ImageEnhance.Brightness(image)
|
enhancer = ImageEnhance.Brightness(image)
|
||||||
return enhancer.enhance(factor)
|
return enhancer.enhance(factor)
|
||||||
|
|
||||||
# 色彩
|
# 色彩
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def apply_color(image: Image.Image, factor: float = 1.5) -> Image.Image:
|
def apply_color(image: Image.Image, factor: float = 1.5) -> Image.Image:
|
||||||
|
if image.mode != 'RGBA':
|
||||||
|
image = image.convert('RGBA')
|
||||||
enhancer = ImageEnhance.Color(image)
|
enhancer = ImageEnhance.Color(image)
|
||||||
return enhancer.enhance(factor)
|
return enhancer.enhance(factor)
|
||||||
|
|
||||||
@ -412,6 +440,13 @@ class ImageFilterImplement:
|
|||||||
if valid_mask.any():
|
if valid_mask.any():
|
||||||
# 使用花式索引复制像素
|
# 使用花式索引复制像素
|
||||||
result[valid_mask] = arr[new_y[valid_mask], new_x[valid_mask]]
|
result[valid_mask] = arr[new_y[valid_mask], new_x[valid_mask]]
|
||||||
|
|
||||||
|
# 计算裁剪边界
|
||||||
|
ys, xs = np.where(valid_mask)
|
||||||
|
min_y, max_y = ys.min(), ys.max() + 1
|
||||||
|
min_x, max_x = xs.min(), xs.max() + 1
|
||||||
|
# 裁剪结果图像
|
||||||
|
result = result[min_y:max_y, min_x:max_x]
|
||||||
|
|
||||||
return Image.fromarray(result, 'RGBA')
|
return Image.fromarray(result, 'RGBA')
|
||||||
|
|
||||||
@ -548,6 +583,18 @@ class ImageFilterImplement:
|
|||||||
|
|
||||||
return Image.fromarray(blended, 'RGBA')
|
return Image.fromarray(blended, 'RGBA')
|
||||||
|
|
||||||
|
# 两张图像直接覆盖
|
||||||
|
@staticmethod
|
||||||
|
def apply_overlay(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||||
|
if image1.mode != 'RGBA':
|
||||||
|
image1 = image1.convert('RGBA')
|
||||||
|
if image2.mode != 'RGBA':
|
||||||
|
image2 = image2.convert('RGBA')
|
||||||
|
|
||||||
|
image2 = image2.resize(image1.size, Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
return Image.alpha_composite(image1, image2)
|
||||||
|
|
||||||
# 叠加渐变色
|
# 叠加渐变色
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def apply_gradient_overlay(
|
def apply_gradient_overlay(
|
||||||
@ -560,6 +607,18 @@ class ImageFilterImplement:
|
|||||||
gradient = gradient_gen.create_gradient(image.size[0], image.size[1], color_nodes)
|
gradient = gradient_gen.create_gradient(image.size[0], image.size[1], color_nodes)
|
||||||
return ImageFilterImplement.apply_blend(image, gradient, mode=overlay_mode, alpha=0.5)
|
return ImageFilterImplement.apply_blend(image, gradient, mode=overlay_mode, alpha=0.5)
|
||||||
|
|
||||||
|
# 生成颜色,类似固态层
|
||||||
|
@staticmethod
|
||||||
|
def generate_solid(
|
||||||
|
image: Image.Image,
|
||||||
|
color_list: str = '[rgb(255,0,0)|(0,0),rgb(0,255,0)|(0,100),rgb(0,0,255)|(50,100)]'
|
||||||
|
):
|
||||||
|
gradient_gen = GradientGenerator()
|
||||||
|
color_nodes = gradient_gen.parse_color_list(color_list)
|
||||||
|
gradient = gradient_gen.create_gradient(image.size[0], image.size[1], color_nodes)
|
||||||
|
return gradient
|
||||||
|
|
||||||
|
|
||||||
# 阴影
|
# 阴影
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def apply_shadow(image: Image.Image,
|
def apply_shadow(image: Image.Image,
|
||||||
@ -567,23 +626,30 @@ class ImageFilterImplement:
|
|||||||
y_offset: int = 10,
|
y_offset: int = 10,
|
||||||
blur: float = 10,
|
blur: float = 10,
|
||||||
opacity: float = 0.5,
|
opacity: float = 0.5,
|
||||||
shadow_color = "black") -> Image.Image:
|
shadow_color: str = "black") -> Image.Image:
|
||||||
if image.mode != 'RGBA':
|
if image.mode != 'RGBA':
|
||||||
image = image.convert('RGBA')
|
image = image.convert('RGBA')
|
||||||
offset = (x_offset, y_offset)
|
offset = (-x_offset, -y_offset)
|
||||||
# 创建阴影图层
|
# 创建阴影图层
|
||||||
shadow = Image.new('RGBA', image.size, (0,0,0,0))
|
|
||||||
shadow_rgb = ColorHandle.parse_color(shadow_color)
|
shadow_rgb = ColorHandle.parse_color(shadow_color)
|
||||||
shadow_draw = Image.new('RGBA', image.size, shadow_rgb + (0,))
|
logger.debug(f"Shadow color RGB: {shadow_rgb}")
|
||||||
alpha = image.split()[3].point(lambda p: int(p * opacity))
|
shadow = Image.new('RGBA', image.size, (0, 0, 0, 0))
|
||||||
shadow.paste(shadow_draw, (0,0), alpha)
|
shadow_draw = ImageDraw.Draw(shadow)
|
||||||
|
# 填充阴影颜色
|
||||||
|
shadow_draw.rectangle([(0, 0), image.size], fill=(shadow_rgb[0], shadow_rgb[1], shadow_rgb[2], 255))
|
||||||
|
# 应用遮罩
|
||||||
|
alpha = image.split()[-1].point(lambda p: p * opacity)
|
||||||
|
shadow.putalpha(alpha)
|
||||||
|
# 移动
|
||||||
|
shadow = shadow.transform(
|
||||||
|
shadow.size,
|
||||||
|
Image.AFFINE,
|
||||||
|
(1, 0, offset[0], 0, 1, offset[1])
|
||||||
|
)
|
||||||
|
# 应用模糊
|
||||||
shadow = shadow.filter(ImageFilter.GaussianBlur(blur))
|
shadow = shadow.filter(ImageFilter.GaussianBlur(blur))
|
||||||
# 创建结果图像
|
# 将阴影叠加在原图下方
|
||||||
result = Image.new('RGBA', (image.width + abs(offset[0]), image.height + abs(offset[1])), (0,0,0,0))
|
result = ImageFilterImplement.apply_overlay(shadow, image)
|
||||||
shadow_position = (max(offset[0],0), max(offset[1],0))
|
|
||||||
image_position = (max(-offset[0],0), max(-offset[1],0))
|
|
||||||
result.paste(shadow, shadow_position, shadow)
|
|
||||||
result.paste(image, image_position, image)
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@ -1040,8 +1106,6 @@ class ImageFilterImplement:
|
|||||||
if image.mode != 'RGBA':
|
if image.mode != 'RGBA':
|
||||||
image = image.convert('RGBA')
|
image = image.convert('RGBA')
|
||||||
|
|
||||||
width, height = image.size
|
|
||||||
arr = np.array(image)
|
|
||||||
# 生成随机偏移
|
# 生成随机偏移
|
||||||
x_offset = random.randint(-max_offset, max_offset)
|
x_offset = random.randint(-max_offset, max_offset)
|
||||||
y_offset = random.randint(-max_offset, max_offset)
|
y_offset = random.randint(-max_offset, max_offset)
|
||||||
@ -1079,6 +1143,191 @@ class ImageFilterImplement:
|
|||||||
|
|
||||||
return Image.fromarray(result, 'RGBA')
|
return Image.fromarray(result, 'RGBA')
|
||||||
|
|
||||||
|
# 描边
|
||||||
|
@staticmethod
|
||||||
|
def apply_stroke(image: Image.Image, stroke_width: int = 5, stroke_color: str = 'black') -> Image.Image:
|
||||||
|
if image.mode != 'RGBA':
|
||||||
|
image = image.convert('RGBA')
|
||||||
|
|
||||||
|
# 基于图像的 alpha 通道创建描边
|
||||||
|
alpha = image.split()[3]
|
||||||
|
# 创建描边图像
|
||||||
|
stroke_image = Image.new('RGBA', image.size, (0, 0, 0, 0))
|
||||||
|
# 根据 alpha 通道的值,以每个像素为中心,扩大描边区域
|
||||||
|
expanded_alpha = alpha.filter(ImageFilter.MaxFilter(size=stroke_width*2+1))
|
||||||
|
draw = Image.new('RGBA', image.size, ColorHandle.parse_color(stroke_color) + (255,))
|
||||||
|
stroke_image.paste(draw, (0, 0), expanded_alpha)
|
||||||
|
# 将描边和原图合并
|
||||||
|
result = Image.alpha_composite(stroke_image, image)
|
||||||
|
return result
|
||||||
|
|
||||||
|
# 基于形状的描边
|
||||||
|
@staticmethod
|
||||||
|
def apply_shape_stroke(image: Image.Image, stroke_width: int = 5, stroke_color: str = 'black', roughness: float = None) -> Image.Image:
|
||||||
|
if image.mode != 'RGBA':
|
||||||
|
image = image.convert('RGBA')
|
||||||
|
|
||||||
|
img = cv2.cvtColor(np.array(image), cv2.COLOR_RGBA2BGRA)
|
||||||
|
|
||||||
|
# 提取alpha通道
|
||||||
|
alpha = img[:, :, 3]
|
||||||
|
|
||||||
|
# 应用阈值创建二值掩码
|
||||||
|
_, binary_mask = cv2.threshold(alpha, 0.5, 255, cv2.THRESH_BINARY)
|
||||||
|
|
||||||
|
# 寻找轮廓
|
||||||
|
contours, hierarchy = cv2.findContours(
|
||||||
|
binary_mask,
|
||||||
|
cv2.RETR_EXTERNAL,
|
||||||
|
cv2.CHAIN_APPROX_SIMPLE
|
||||||
|
)
|
||||||
|
|
||||||
|
# 减少轮廓点数,以实现尖角效果
|
||||||
|
if roughness is not None:
|
||||||
|
epsilon = roughness * cv2.arcLength(contours[0], True)
|
||||||
|
contours = [cv2.approxPolyDP(cnt, epsilon, True) for cnt in contours]
|
||||||
|
|
||||||
|
# 将轮廓点沿法线方向外扩
|
||||||
|
expanded_contours = expand_contours(contours, stroke_width)
|
||||||
|
|
||||||
|
# 创建描边图像
|
||||||
|
stroke_img = np.zeros_like(img)
|
||||||
|
|
||||||
|
# 如果没有轮廓,直接返回原图
|
||||||
|
if not expanded_contours[0].any():
|
||||||
|
return image
|
||||||
|
cv2.fillPoly(stroke_img, expanded_contours, ColorHandle.parse_color(stroke_color) + (255,))
|
||||||
|
|
||||||
|
# 轮廓图像转为PIL格式
|
||||||
|
stroke_pil = Image.fromarray(cv2.cvtColor(stroke_img, cv2.COLOR_BGRA2RGBA))
|
||||||
|
# 合并描边和原图
|
||||||
|
result = Image.alpha_composite(stroke_pil, image)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
# 半调
|
||||||
|
@staticmethod
|
||||||
|
def apply_halftone(image: Image.Image, dot_size: int = 5) -> Image.Image:
|
||||||
|
if image.mode != 'L':
|
||||||
|
grayscale = image.convert('L')
|
||||||
|
else:
|
||||||
|
grayscale = image.copy()
|
||||||
|
|
||||||
|
# 获取图像尺寸
|
||||||
|
width, height = grayscale.size
|
||||||
|
|
||||||
|
# 计算网格数量
|
||||||
|
grid_width = math.ceil(width / dot_size)
|
||||||
|
grid_height = math.ceil(height / dot_size)
|
||||||
|
|
||||||
|
# 创建输出图像(白色背景)
|
||||||
|
output = Image.new('RGB', (width, height), 'white')
|
||||||
|
draw = ImageDraw.Draw(output)
|
||||||
|
|
||||||
|
# 遍历每个网格单元
|
||||||
|
for gy in range(grid_height):
|
||||||
|
for gx in range(grid_width):
|
||||||
|
# 计算当前网格的边界
|
||||||
|
left = gx * dot_size
|
||||||
|
top = gy * dot_size
|
||||||
|
right = min(left + dot_size, width)
|
||||||
|
bottom = min(top + dot_size, height)
|
||||||
|
|
||||||
|
# 获取当前网格的区域
|
||||||
|
grid_region = grayscale.crop((left, top, right, bottom))
|
||||||
|
|
||||||
|
# 计算网格内像素的平均亮度(0-255)
|
||||||
|
stat = ImageStat.Stat(grid_region)
|
||||||
|
avg_brightness = stat.mean[0]
|
||||||
|
|
||||||
|
# 将亮度转换为网点半径
|
||||||
|
# 亮度越高(越白),网点越小;亮度越低(越黑),网点越大
|
||||||
|
max_radius = dot_size / 2
|
||||||
|
radius = max_radius * (1 - avg_brightness / 255)
|
||||||
|
|
||||||
|
# 如果半径太小,则不绘制网点
|
||||||
|
if radius > 0.5:
|
||||||
|
# 计算网点中心坐标
|
||||||
|
center_x = left + (right - left) / 2
|
||||||
|
center_y = top + (bottom - top) / 2
|
||||||
|
|
||||||
|
# 绘制黑色网点
|
||||||
|
draw.ellipse([
|
||||||
|
center_x - radius + 0.5,
|
||||||
|
center_y - radius + 0.5,
|
||||||
|
center_x + radius + 0.5,
|
||||||
|
center_y + radius + 0.5
|
||||||
|
], fill='black')
|
||||||
|
|
||||||
|
# 叠加 alpha 通道
|
||||||
|
if image.mode == 'RGBA':
|
||||||
|
alpha = image.split()[3]
|
||||||
|
output.putalpha(alpha)
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
# 设置通道
|
||||||
|
@staticmethod
|
||||||
|
def apply_set_channel(image: Image.Image, apply_image: Image.Image, channel: str = 'A') -> Image.Image:
|
||||||
|
if image.mode != 'RGBA':
|
||||||
|
image = image.convert('RGBA')
|
||||||
|
|
||||||
|
if apply_image.mode != 'RGBA':
|
||||||
|
apply_image = apply_image.convert('RGBA')
|
||||||
|
|
||||||
|
# 将 apply_image 的通道设置给 image
|
||||||
|
image_arr = np.array(image)
|
||||||
|
apply_arr = np.array(apply_image.resize(image.size, Image.Resampling.LANCZOS))
|
||||||
|
channel_index = {'R':0, 'G':1, 'B':2, 'A':3}.get(channel.upper(), 0)
|
||||||
|
image_arr[:, :, channel_index] = apply_arr[:, :, channel_index]
|
||||||
|
|
||||||
|
return Image.fromarray(image_arr, 'RGBA')
|
||||||
|
|
||||||
|
# 设置遮罩
|
||||||
|
def apply_set_mask(image: Image.Image, mask_image: Image.Image) -> Image.Image:
|
||||||
|
if image.mode != 'RGBA':
|
||||||
|
image = image.convert('RGBA')
|
||||||
|
|
||||||
|
if mask_image.mode != 'L':
|
||||||
|
mask_image = mask_image.convert('L')
|
||||||
|
|
||||||
|
# 应用遮罩
|
||||||
|
image.putalpha(mask_image.resize(image.size, Image.Resampling.LANCZOS))
|
||||||
|
|
||||||
|
return image
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_empty(image: Image.Image, images: list[Image.Image], width: int = 512, height: int = 512) -> Image.Image:
|
||||||
|
# 生成空白图像
|
||||||
|
empty_image = Image.new('RGBA', (width, height), (255, 255, 255, 0))
|
||||||
|
images.append(empty_image)
|
||||||
|
return image
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_text(image: Image.Image, images: list[Image.Image],
|
||||||
|
text: str = "请输入文本",
|
||||||
|
font_size: int = 32,
|
||||||
|
font_color: str = "black",
|
||||||
|
font_path: str = "HarmonyOS_Sans_SC_Regular.ttf") -> Image.Image:
|
||||||
|
# 生成文本图像
|
||||||
|
font = ImageFont.truetype(FONTS_PATH / font_path, font_size)
|
||||||
|
# 获取文本边界框
|
||||||
|
padding = 10
|
||||||
|
temp_draw = ImageDraw.Draw(Image.new('RGBA', (1,1)))
|
||||||
|
bbox = temp_draw.textbbox((0, 0), text, font=font)
|
||||||
|
text_width = bbox[2] - bbox[0] + padding * 2
|
||||||
|
text_height = bbox[3] - bbox[1] + padding * 2
|
||||||
|
# 创建文本图像
|
||||||
|
text_image = Image.new('RGBA', (text_width, text_height), (255, 255, 255, 0))
|
||||||
|
draw = ImageDraw.Draw(text_image)
|
||||||
|
draw_x = padding - bbox[0]
|
||||||
|
draw_y = padding - bbox[1]
|
||||||
|
draw.text((draw_x,draw_y), text, font=font, fill=ColorHandle.parse_color(font_color) + (255,))
|
||||||
|
images.append(text_image)
|
||||||
|
return image
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ImageFilterEmpty:
|
class ImageFilterEmpty:
|
||||||
# 空滤镜,不做任何处理,形式化参数用
|
# 空滤镜,不做任何处理,形式化参数用
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -1090,17 +1339,50 @@ class ImageFilterEmpty:
|
|||||||
return image
|
return image
|
||||||
|
|
||||||
class ImageFilterStorage:
|
class ImageFilterStorage:
|
||||||
# 用于存储图像
|
# 用于暂存图像,存储在第一项的后面
|
||||||
@staticmethod
|
|
||||||
def store_image(image: Image.Image, name: str, sender_info: SenderInfo) -> Image.Image:
|
|
||||||
ImageStorager.save_image(image, name, sender_info.group_id, sender_info.qq_id)
|
|
||||||
return image
|
|
||||||
|
|
||||||
# 用于暂存图像
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def temp_store_image(image: Image.Image, images: list[Image.Image]) -> Image.Image:
|
def temp_store_image(image: Image.Image, images: list[Image.Image]) -> Image.Image:
|
||||||
images.append(image)
|
images.insert(1, image.copy())
|
||||||
return image
|
return image
|
||||||
|
|
||||||
|
# 用于交换移动索引
|
||||||
|
@staticmethod
|
||||||
|
def swap_image_index(image: Image.Image, images: list[Image.Image], src: int = 2, dest: int = 1) -> Image.Image:
|
||||||
|
if len(images) == 0:
|
||||||
|
return image
|
||||||
|
# 将二者交换
|
||||||
|
src -= 1
|
||||||
|
dest -= 1
|
||||||
|
if 0 <= src < len(images) and 0 <= dest < len(images):
|
||||||
|
images[src], images[dest] = images[dest], images[src]
|
||||||
|
return images[0]
|
||||||
|
|
||||||
|
# 用于删除指定索引的图像
|
||||||
|
@staticmethod
|
||||||
|
def delete_image_by_index(image: Image.Image, images: list[Image.Image], index: int = 1) -> Image.Image:
|
||||||
|
# 以 1 为基准
|
||||||
|
index -= 1
|
||||||
|
if len(images) == 1:
|
||||||
|
# 只有一张图像,不能删除
|
||||||
|
return image
|
||||||
|
if 0 <= index < len(images):
|
||||||
|
del images[index]
|
||||||
|
return images[0]
|
||||||
|
|
||||||
|
# 用于选择指定索引的图像为首图
|
||||||
|
@staticmethod
|
||||||
|
def select_image_by_index(image: Image.Image, images: list[Image.Image], index: int = 2) -> Image.Image:
|
||||||
|
# 以 1 为基准
|
||||||
|
index -= 1
|
||||||
|
if 0 <= index < len(images):
|
||||||
|
return images[index]
|
||||||
|
return image
|
||||||
|
|
||||||
|
# 用于存储图像
|
||||||
|
@staticmethod
|
||||||
|
def store_image(image: Image.Image, name: str, sender_info: SenderInfo) -> StoredInfo:
|
||||||
|
ImageStorager.save_image_by_pil(image, name, sender_info.group_id, sender_info.qq_id)
|
||||||
|
return StoredInfo(name=name)
|
||||||
|
|
||||||
# 用于读取图像
|
# 用于读取图像
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@ -47,17 +47,41 @@ class ImageFilterManager:
|
|||||||
"晃动": ImageFilterImplement.apply_random_wiggle,
|
"晃动": ImageFilterImplement.apply_random_wiggle,
|
||||||
"动图": ImageFilterEmpty.empty_filter_param,
|
"动图": ImageFilterEmpty.empty_filter_param,
|
||||||
"像素抖动": ImageFilterImplement.apply_pixel_jitter,
|
"像素抖动": ImageFilterImplement.apply_pixel_jitter,
|
||||||
|
"描边": ImageFilterImplement.apply_stroke,
|
||||||
|
"形状描边": ImageFilterImplement.apply_shape_stroke,
|
||||||
|
"半调": ImageFilterImplement.apply_halftone,
|
||||||
|
"设置通道": ImageFilterImplement.apply_set_channel,
|
||||||
|
"设置遮罩": ImageFilterImplement.apply_set_mask,
|
||||||
|
# 图像处理
|
||||||
"存入图像": ImageFilterStorage.store_image,
|
"存入图像": ImageFilterStorage.store_image,
|
||||||
"读取图像": ImageFilterStorage.load_image,
|
"读取图像": ImageFilterStorage.load_image,
|
||||||
"暂存图像": ImageFilterStorage.temp_store_image,
|
"暂存图像": ImageFilterStorage.temp_store_image,
|
||||||
|
"交换图像": ImageFilterStorage.swap_image_index,
|
||||||
|
"删除图像": ImageFilterStorage.delete_image_by_index,
|
||||||
|
"选择图像": ImageFilterStorage.select_image_by_index,
|
||||||
|
# 多图像处理
|
||||||
|
"混合图像": ImageFilterImplement.apply_blend,
|
||||||
|
"覆盖图像": ImageFilterImplement.apply_overlay,
|
||||||
|
# 生成式
|
||||||
|
"覆加颜色": ImageFilterImplement.generate_solid,
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_filter_map = {
|
||||||
|
"生成图层": ImageFilterImplement.generate_empty,
|
||||||
|
"生成文本": ImageFilterImplement.generate_text
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_filter(cls, name: str) -> Optional[callable]:
|
def get_filter(cls, name: str) -> Optional[callable]:
|
||||||
return cls.filter_map.get(name)
|
if name in cls.filter_map:
|
||||||
|
return cls.filter_map[name]
|
||||||
|
elif name in cls.generate_filter_map:
|
||||||
|
return cls.generate_filter_map[name]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def has_filter(cls, name: str) -> bool:
|
def has_filter(cls, name: str) -> bool:
|
||||||
return name in cls.filter_map
|
return name in cls.filter_map or name in cls.generate_filter_map
|
||||||
|
|
||||||
|
|
||||||
@ -14,20 +14,23 @@ class GradientGenerator:
|
|||||||
"""解析渐变颜色列表字符串
|
"""解析渐变颜色列表字符串
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
color_list_str: 格式如 '[rgb(255,0,0)|(0,0),rgb(0,255,0)|(0,100),rgb(0,0,255)|(50,100)]'
|
color_list_str: 格式如 '[rgb(255,0,0)|(0,0)+rgb(0,255,0)|(0,100)+rgb(0,0,255)|(50,100)]'
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list: 包含颜色和位置信息的字典列表
|
list: 包含颜色和位置信息的字典列表
|
||||||
"""
|
"""
|
||||||
color_nodes = []
|
color_nodes = []
|
||||||
color_list_str = color_list_str.strip('[] ').strip()
|
color_list_str = color_list_str.strip('[]').strip()
|
||||||
pattern = r'([^|]+)\|\(([^)]+)\)'
|
matches = color_list_str.split('+')
|
||||||
matches = re.findall(pattern, color_list_str)
|
|
||||||
|
|
||||||
for color_str, pos_str in matches:
|
for single_str in matches:
|
||||||
|
color_str = single_str.split('|')[0]
|
||||||
|
pos_str = single_str.split('|')[1] if '|' in single_str else '0,0'
|
||||||
|
|
||||||
color = ColorHandle.parse_color(color_str.strip())
|
color = ColorHandle.parse_color(color_str.strip())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
pos_str = pos_str.replace('(', '').replace(')', '')
|
||||||
x_str, y_str = pos_str.split(',')
|
x_str, y_str = pos_str.split(',')
|
||||||
x_percent = float(x_str.strip().replace('%', ''))
|
x_percent = float(x_str.strip().replace('%', ''))
|
||||||
y_percent = float(y_str.strip().replace('%', ''))
|
y_percent = float(y_str.strip().replace('%', ''))
|
||||||
|
|||||||
@ -8,12 +8,13 @@ from nonebot_plugin_apscheduler import driver
|
|||||||
from konabot.common.path import DATA_PATH
|
from konabot.common.path import DATA_PATH
|
||||||
import os
|
import os
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
IMAGE_PATH = DATA_PATH / "temp" / "images"
|
IMAGE_PATH = DATA_PATH / "temp" / "images"
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ImageResource:
|
class ImageResource:
|
||||||
name: str
|
filename: str
|
||||||
expire: int
|
expire: int
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -39,6 +40,14 @@ class ImageStorager:
|
|||||||
if resource_path.exists():
|
if resource_path.exists():
|
||||||
os.remove(resource_path)
|
os.remove(resource_path)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def clear_all_image():
|
||||||
|
# 清理 temp 目录下的所有图片资源
|
||||||
|
for file in os.listdir(IMAGE_PATH):
|
||||||
|
file_path = IMAGE_PATH / file
|
||||||
|
if file_path.is_file():
|
||||||
|
os.remove(file_path)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def clear_expire_image(cls):
|
async def clear_expire_image(cls):
|
||||||
# 清理过期的图片资源,将未被删除的放入列表中,如果超过最大数量则删除最早过期的
|
# 清理过期的图片资源,将未被删除的放入列表中,如果超过最大数量则删除最早过期的
|
||||||
@ -63,15 +72,17 @@ class ImageStorager:
|
|||||||
resource = ImageStorager.images_pool[name].resources[group_id][qq_id]
|
resource = ImageStorager.images_pool[name].resources[group_id][qq_id]
|
||||||
del ImageStorager.images_pool[name].resources[group_id][qq_id]
|
del ImageStorager.images_pool[name].resources[group_id][qq_id]
|
||||||
cls.delete_path_image(name)
|
cls.delete_path_image(name)
|
||||||
|
logger.info("过期图片清理完成")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _add_to_pool(cls, image: bytes, name: str, group_id: str, qq_id: str, expire: int = 36000):
|
def _add_to_pool(cls, filename: str, name: str, group_id: str, qq_id: str, expire: int = 36000):
|
||||||
expire_time = time.time() + expire
|
expire_time = time.time() + expire
|
||||||
if name not in cls.images_pool:
|
if name not in cls.images_pool:
|
||||||
cls.images_pool[name] = StorageImage(name=name,resources={})
|
cls.images_pool[name] = StorageImage(name=name,resources={})
|
||||||
if group_id not in cls.images_pool[name].resources:
|
if group_id not in cls.images_pool[name].resources:
|
||||||
cls.images_pool[name].resources[group_id] = {}
|
cls.images_pool[name].resources[group_id] = {}
|
||||||
cls.images_pool[name].resources[group_id][qq_id] = ImageResource(name=name, expire=expire_time)
|
cls.images_pool[name].resources[group_id][qq_id] = ImageResource(filename=filename, expire=expire_time)
|
||||||
|
logger.debug(f"{cls.images_pool}")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def save_image(cls, image: bytes, name: str, group_id: str, qq_id: str) -> None:
|
def save_image(cls, image: bytes, name: str, group_id: str, qq_id: str) -> None:
|
||||||
@ -88,20 +99,39 @@ class ImageStorager:
|
|||||||
with open(full_path, "wb") as f:
|
with open(full_path, "wb") as f:
|
||||||
f.write(image)
|
f.write(image)
|
||||||
# 将文件写入 images_pool
|
# 将文件写入 images_pool
|
||||||
cls._add_to_pool(image, file_name, group_id, qq_id)
|
logger.debug(f"Image saved: {file_name} for group {group_id}, qq {qq_id}")
|
||||||
|
cls._add_to_pool(file_name, name, group_id, qq_id)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def save_image_by_pil(cls, image: Image.Image, name: str, group_id: str, qq_id: str) -> None:
|
||||||
|
"""
|
||||||
|
以哈希值命名保存图片,并返回图片资源信息
|
||||||
|
"""
|
||||||
|
img_byte_arr = BytesIO()
|
||||||
|
# 如果图片是动图,保存为 GIF 格式
|
||||||
|
if getattr(image, "is_animated", False):
|
||||||
|
image.save(img_byte_arr, format="GIF", save_all=True, loop=0)
|
||||||
|
else:
|
||||||
|
image.save(img_byte_arr, format=image.format or "PNG")
|
||||||
|
img_bytes = img_byte_arr.getvalue()
|
||||||
|
cls.save_image(img_bytes, name, group_id, qq_id)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load_image(cls, name: str, group_id: str, qq_id: str) -> Image:
|
def load_image(cls, name: str, group_id: str, qq_id: str) -> Image:
|
||||||
|
logger.debug(f"Loading image: {name} for group {group_id}, qq {qq_id}")
|
||||||
if name not in cls.images_pool:
|
if name not in cls.images_pool:
|
||||||
|
logger.debug(f"Image {name} not found in pool")
|
||||||
return None
|
return None
|
||||||
if group_id not in cls.images_pool[name].resources:
|
if group_id not in cls.images_pool[name].resources:
|
||||||
|
logger.debug(f"No resources for group {group_id} in image {name}")
|
||||||
return None
|
return None
|
||||||
# 寻找对应 QQ 号 的资源,如果没有就返回相同群下的第一个资源
|
# 寻找对应 QQ 号 的资源,如果没有就返回相同群下的第一个资源
|
||||||
if qq_id not in cls.images_pool[name].resources[group_id]:
|
if qq_id not in cls.images_pool[name].resources[group_id]:
|
||||||
first_qq_id = next(iter(cls.images_pool[name].resources[group_id]))
|
first_qq_id = next(iter(cls.images_pool[name].resources[group_id]))
|
||||||
qq_id = first_qq_id
|
qq_id = first_qq_id
|
||||||
resource = cls.images_pool[name].resources[group_id][qq_id]
|
resource = cls.images_pool[name].resources[group_id][qq_id]
|
||||||
resource_path = IMAGE_PATH / resource.name
|
resource_path = IMAGE_PATH / resource.filename
|
||||||
|
logger.debug(f"Image path: {resource_path}")
|
||||||
return Image.open(resource_path)
|
return Image.open(resource_path)
|
||||||
|
|
||||||
class ImageStoragerManager:
|
class ImageStoragerManager:
|
||||||
@ -112,6 +142,8 @@ class ImageStoragerManager:
|
|||||||
|
|
||||||
async def start_auto_clear(self):
|
async def start_auto_clear(self):
|
||||||
"""启动自动任务"""
|
"""启动自动任务"""
|
||||||
|
# 先清理一次
|
||||||
|
await ImageStorager.clear_all_image()
|
||||||
self._running = True
|
self._running = True
|
||||||
self._clear_task = asyncio.create_task(self._auto_clear_loop())
|
self._clear_task = asyncio.create_task(self._auto_clear_loop())
|
||||||
|
|
||||||
|
|||||||
125
konabot/plugins/fx_process/math_helper.py
Normal file
125
konabot/plugins/fx_process/math_helper.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import cv2
|
||||||
|
from nonebot import logger
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from shapely.geometry import Polygon
|
||||||
|
from shapely.ops import unary_union
|
||||||
|
|
||||||
|
def fix_with_shapely(contours: list) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
使用Shapely库处理复杂自相交
|
||||||
|
"""
|
||||||
|
fixed_polygons = []
|
||||||
|
for contour in contours:
|
||||||
|
# 转换输入为正确的格式
|
||||||
|
contour_array = contour.reshape(-1, 2)
|
||||||
|
# 转换为Shapely多边形
|
||||||
|
polygon = Polygon(contour_array)
|
||||||
|
if not polygon.is_valid:
|
||||||
|
polygon = polygon.buffer(0)
|
||||||
|
fixed_polygons.append(polygon)
|
||||||
|
# 接下来把所有轮廓合并为一个
|
||||||
|
if len(fixed_polygons) >= 1:
|
||||||
|
merged_polygon = unary_union(fixed_polygons)
|
||||||
|
if merged_polygon.geom_type == 'Polygon':
|
||||||
|
merged_points = np.array(merged_polygon.exterior.coords, dtype=np.int32)
|
||||||
|
elif merged_polygon.geom_type == 'MultiPolygon':
|
||||||
|
largest = max(merged_polygon.geoms, key=lambda p: p.area)
|
||||||
|
merged_points = np.array(largest.exterior.coords, dtype=np.int32)
|
||||||
|
return [merged_points.reshape(-1, 1, 2)]
|
||||||
|
else:
|
||||||
|
logger.warning("No valid contours found after fixing with Shapely.")
|
||||||
|
return [np.array([], dtype=np.int32).reshape(0, 1, 2)]
|
||||||
|
|
||||||
|
def expand_contours(contours, stroke_width):
|
||||||
|
"""
|
||||||
|
将轮廓向外扩展指定宽度
|
||||||
|
|
||||||
|
参数:
|
||||||
|
contours: OpenCV轮廓列表
|
||||||
|
stroke_width: 扩展宽度(像素)
|
||||||
|
|
||||||
|
返回:
|
||||||
|
扩展后的轮廓列表
|
||||||
|
"""
|
||||||
|
expanded_contours = []
|
||||||
|
|
||||||
|
for cnt in contours:
|
||||||
|
# 将轮廓转换为点列表
|
||||||
|
points = cnt.reshape(-1, 2).astype(np.float32)
|
||||||
|
n = len(points)
|
||||||
|
|
||||||
|
if n < 3:
|
||||||
|
continue # 至少需要3个点才能形成多边形
|
||||||
|
|
||||||
|
expanded_points = []
|
||||||
|
|
||||||
|
for i in range(n):
|
||||||
|
# 获取当前点、前一个点和后一个点
|
||||||
|
p_curr = points[i]
|
||||||
|
p_prev = points[(i - 1) % n]
|
||||||
|
p_next = points[(i + 1) % n]
|
||||||
|
|
||||||
|
# 计算两条边的向量
|
||||||
|
v1 = p_curr - p_prev # 前一条边(从prev到curr)
|
||||||
|
v2 = p_next - p_curr # 后一条边(从curr到next)
|
||||||
|
|
||||||
|
# 归一化
|
||||||
|
norm1 = np.linalg.norm(v1)
|
||||||
|
norm2 = np.linalg.norm(v2)
|
||||||
|
|
||||||
|
if norm1 == 0 or norm2 == 0:
|
||||||
|
# 如果有零向量,直接沿着法线方向扩展
|
||||||
|
edge_dir = np.array([0, 0])
|
||||||
|
if norm1 > 0:
|
||||||
|
edge_dir = v1 / norm1
|
||||||
|
elif norm2 > 0:
|
||||||
|
edge_dir = v2 / norm2
|
||||||
|
normal = np.array([-edge_dir[1], edge_dir[0]])
|
||||||
|
expanded_point = p_curr + normal * stroke_width
|
||||||
|
else:
|
||||||
|
# 归一化向量
|
||||||
|
v1_norm = v1 / norm1
|
||||||
|
v2_norm = v2 / norm2
|
||||||
|
|
||||||
|
# 计算两条边的单位法向量(指向多边形外部)
|
||||||
|
n1 = np.array([-v1_norm[1], v1_norm[0]])
|
||||||
|
n2 = np.array([-v2_norm[1], v2_norm[0]])
|
||||||
|
|
||||||
|
# 计算角平分线方向(两个法向量的和)
|
||||||
|
bisector = n1 + n2
|
||||||
|
|
||||||
|
# 计算平分线的长度
|
||||||
|
bisector_norm = np.linalg.norm(bisector)
|
||||||
|
|
||||||
|
if bisector_norm == 0:
|
||||||
|
# 如果两条边平行(同向或反向),取任一法线方向
|
||||||
|
expanded_point = p_curr + n1 * stroke_width
|
||||||
|
else:
|
||||||
|
# 归一化平分线
|
||||||
|
bisector_normalized = bisector / bisector_norm
|
||||||
|
|
||||||
|
# 计算偏移距离(考虑夹角)
|
||||||
|
# 使用余弦定理计算正确的偏移距离
|
||||||
|
cos_angle = np.dot(v1_norm, v2_norm)
|
||||||
|
angle = np.arccos(np.clip(cos_angle, -1.0, 1.0))
|
||||||
|
|
||||||
|
if abs(np.pi - angle) < 1e-6: # 近似平角
|
||||||
|
# 接近直线的情况
|
||||||
|
offset_distance = stroke_width
|
||||||
|
else:
|
||||||
|
# 计算正确的偏移距离
|
||||||
|
offset_distance = stroke_width / np.sin(angle / 2)
|
||||||
|
|
||||||
|
# 计算扩展点
|
||||||
|
expanded_point = p_curr + bisector_normalized * offset_distance
|
||||||
|
|
||||||
|
expanded_points.append(expanded_point)
|
||||||
|
|
||||||
|
# 将扩展后的点转换为整数坐标
|
||||||
|
expanded_cnt = np.array(expanded_points, dtype=np.float32).reshape(-1, 1, 2)
|
||||||
|
expanded_contours.append(expanded_cnt.astype(np.int32))
|
||||||
|
|
||||||
|
expanded_contours = fix_with_shapely(expanded_contours)
|
||||||
|
|
||||||
|
return expanded_contours
|
||||||
@ -10,10 +10,13 @@ class FilterItem:
|
|||||||
class ImageRequireSignal:
|
class ImageRequireSignal:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ImagesListRequireSignal:
|
class ImagesListRequireSignal:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StoredInfo:
|
||||||
|
name: str
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SenderInfo:
|
class SenderInfo:
|
||||||
group_id: str
|
group_id: str
|
||||||
|
|||||||
126
konabot/plugins/handle_text/__init__.py
Normal file
126
konabot/plugins/handle_text/__init__.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
from typing import cast
|
||||||
|
from loguru import logger
|
||||||
|
from nonebot import on_command
|
||||||
|
import nonebot
|
||||||
|
from nonebot.adapters import Event, Bot
|
||||||
|
from nonebot_plugin_alconna import UniMessage, UniMsg
|
||||||
|
from nonebot.adapters.onebot.v11.event import MessageEvent as OB11MessageEvent
|
||||||
|
from nonebot.adapters.onebot.v11.bot import Bot as OB11Bot
|
||||||
|
from nonebot.adapters.onebot.v11.message import Message as OB11Message
|
||||||
|
|
||||||
|
from konabot.common.apis.ali_content_safety import AlibabaGreen
|
||||||
|
from konabot.common.longtask import DepLongTaskTarget
|
||||||
|
from konabot.plugins.handle_text.base import (
|
||||||
|
PipelineRunner,
|
||||||
|
TextHandlerEnvironment,
|
||||||
|
register_text_handlers,
|
||||||
|
)
|
||||||
|
from konabot.plugins.handle_text.handlers.ai_handlers import THQwen
|
||||||
|
from konabot.plugins.handle_text.handlers.encoding_handlers import (
|
||||||
|
THAlign,
|
||||||
|
THAlphaConv,
|
||||||
|
THB64Hex,
|
||||||
|
THBase64,
|
||||||
|
THBaseConv,
|
||||||
|
THCaesar,
|
||||||
|
THMorse,
|
||||||
|
THReverse,
|
||||||
|
)
|
||||||
|
from konabot.plugins.handle_text.handlers.random_handlers import THShuffle, THSorted
|
||||||
|
from konabot.plugins.handle_text.handlers.unix_handlers import (
|
||||||
|
THCat,
|
||||||
|
THEcho,
|
||||||
|
THReplace,
|
||||||
|
THRm,
|
||||||
|
)
|
||||||
|
from konabot.plugins.handle_text.handlers.whitespace_handlers import (
|
||||||
|
THLines,
|
||||||
|
THLTrim,
|
||||||
|
THRTrim,
|
||||||
|
THSqueeze,
|
||||||
|
THTrim,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
cmd = on_command(cmd="textfx", aliases={"处理文字", "处理文本"})
|
||||||
|
|
||||||
|
|
||||||
|
@cmd.handle()
|
||||||
|
async def _(msg: UniMsg, evt: Event, bot: Bot, target: DepLongTaskTarget):
|
||||||
|
istream = ""
|
||||||
|
if isinstance(evt, OB11MessageEvent):
|
||||||
|
if evt.reply is not None:
|
||||||
|
istream = evt.reply.message.extract_plain_text()
|
||||||
|
else:
|
||||||
|
for seg in evt.get_message():
|
||||||
|
if seg.type == "reply":
|
||||||
|
msgid = seg.get("id")
|
||||||
|
if msgid is not None:
|
||||||
|
msg2data = await cast(OB11Bot, bot).get_msg(message_id=msgid)
|
||||||
|
istream = OB11Message(
|
||||||
|
msg2data.get("message")
|
||||||
|
).extract_plain_text()
|
||||||
|
|
||||||
|
script = msg.extract_plain_text().removeprefix("textfx").removeprefix("处理文字")
|
||||||
|
runner = PipelineRunner.get_runner()
|
||||||
|
res = runner.parse_pipeline(script)
|
||||||
|
|
||||||
|
if isinstance(res, str):
|
||||||
|
await target.send_message(res)
|
||||||
|
return
|
||||||
|
|
||||||
|
env = TextHandlerEnvironment(is_trusted=False)
|
||||||
|
results = await runner.run_pipeline(res, istream or None, env)
|
||||||
|
|
||||||
|
# 检查是否有错误
|
||||||
|
for r in results:
|
||||||
|
if r.code != 0:
|
||||||
|
await target.send_message(f"处理指令时出现问题:{r.ostream}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 收集所有组的文本输出和附件
|
||||||
|
ostreams = [r.ostream for r in results if r.ostream is not None]
|
||||||
|
attachments = [r.attachment for r in results if r.attachment is not None]
|
||||||
|
|
||||||
|
if ostreams:
|
||||||
|
txt = "\n".join(ostreams)
|
||||||
|
err = await AlibabaGreen.detect(txt)
|
||||||
|
if not err:
|
||||||
|
await target.send_message(
|
||||||
|
"处理指令时出现问题:内容被拦截!请你检查你的内容是否合理!"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
await target.send_message(txt, at=False)
|
||||||
|
|
||||||
|
for att in attachments:
|
||||||
|
await target.send_message(UniMessage.image(raw=att), at=False)
|
||||||
|
|
||||||
|
|
||||||
|
driver = nonebot.get_driver()
|
||||||
|
|
||||||
|
|
||||||
|
@driver.on_startup
|
||||||
|
async def _():
|
||||||
|
register_text_handlers(
|
||||||
|
THCat(),
|
||||||
|
THEcho(),
|
||||||
|
THRm(),
|
||||||
|
THShuffle(),
|
||||||
|
THReplace(),
|
||||||
|
THBase64(),
|
||||||
|
THCaesar(),
|
||||||
|
THReverse(),
|
||||||
|
THBaseConv(),
|
||||||
|
THAlphaConv(),
|
||||||
|
THB64Hex(),
|
||||||
|
THAlign(),
|
||||||
|
THSorted(),
|
||||||
|
THMorse(),
|
||||||
|
THQwen(),
|
||||||
|
THTrim(),
|
||||||
|
THLTrim(),
|
||||||
|
THRTrim(),
|
||||||
|
THSqueeze(),
|
||||||
|
THLines(),
|
||||||
|
)
|
||||||
|
logger.info(f"注册了 TextHandler:{PipelineRunner.get_runner().handlers}")
|
||||||
348
konabot/plugins/handle_text/base.py
Normal file
348
konabot/plugins/handle_text/base.py
Normal file
@ -0,0 +1,348 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import Enum
|
||||||
|
from string import whitespace
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TextHandlerEnvironment:
|
||||||
|
is_trusted: bool
|
||||||
|
buffers: dict[str, str] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TextHandleResult:
|
||||||
|
code: int
|
||||||
|
ostream: str | None
|
||||||
|
attachment: bytes | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class TextHandler(ABC):
|
||||||
|
name: str = ""
|
||||||
|
keywords: list[str] = []
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def handle(
|
||||||
|
self, env: TextHandlerEnvironment, istream: str | None, args: list[str]
|
||||||
|
) -> TextHandleResult: ...
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<{self.__class__.__name__}: {self.name} [{''.join(self.keywords)}]>"
|
||||||
|
|
||||||
|
|
||||||
|
class TextHandlerSync(TextHandler):
|
||||||
|
@abstractmethod
|
||||||
|
def handle_sync(
|
||||||
|
self, env: TextHandlerEnvironment, istream: str | None, args: list[str]
|
||||||
|
) -> TextHandleResult: ...
|
||||||
|
|
||||||
|
async def handle(
|
||||||
|
self, env: TextHandlerEnvironment, istream: str | None, args: list[str]
|
||||||
|
) -> TextHandleResult:
|
||||||
|
def _hs():
|
||||||
|
return self.handle_sync(env, istream, args)
|
||||||
|
|
||||||
|
return await asyncio.to_thread(_hs)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PipelineCommand:
|
||||||
|
handler: TextHandler
|
||||||
|
args: list[str]
|
||||||
|
# 新增:重定向目标(buffer key)
|
||||||
|
redirect_target: str | None = None
|
||||||
|
# 新增:是否为追加模式 (>>)
|
||||||
|
redirect_append: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Pipeline:
|
||||||
|
command_groups: list[list[PipelineCommand]] = field(default_factory=list)
|
||||||
|
"一个列表的列表,每一组之间的指令之间使用管道符连接,而不同组之间不会有数据流"
|
||||||
|
|
||||||
|
|
||||||
|
class PipelineParseStatus(Enum):
|
||||||
|
normal = 0
|
||||||
|
in_string = 1
|
||||||
|
in_string_to_escape = 2
|
||||||
|
off_string = 3
|
||||||
|
|
||||||
|
|
||||||
|
whitespaces = whitespace + " "
|
||||||
|
|
||||||
|
|
||||||
|
class PipelineRunner:
|
||||||
|
handlers: list[TextHandler]
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.handlers = []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_runner():
|
||||||
|
if "singleton" not in PipelineRunner.__annotations__:
|
||||||
|
PipelineRunner.__annotations__["singleton"] = PipelineRunner()
|
||||||
|
return cast(PipelineRunner, PipelineRunner.__annotations__.get("singleton"))
|
||||||
|
|
||||||
|
def register(self, handler: TextHandler):
|
||||||
|
self.handlers.append(handler)
|
||||||
|
|
||||||
|
def parse_pipeline(self, script: str) -> Pipeline | str:
|
||||||
|
pipeline = Pipeline()
|
||||||
|
|
||||||
|
# 当前正在构建的上下文
|
||||||
|
current_group: list[PipelineCommand] = []
|
||||||
|
current_command_args: list[str] = []
|
||||||
|
|
||||||
|
# 字符串解析状态
|
||||||
|
status = PipelineParseStatus.normal
|
||||||
|
current_string = ""
|
||||||
|
current_string_raw = ""
|
||||||
|
status_in_string_pair = ""
|
||||||
|
has_token = False # 是否正在构建一个 token(区分空字符串和无 token)
|
||||||
|
|
||||||
|
# 重定向解析状态
|
||||||
|
is_parsing_redirect_filename = False
|
||||||
|
current_redirect_target: str | None = None
|
||||||
|
current_redirect_append = False
|
||||||
|
|
||||||
|
# 辅助函数:将当前解析到的字符串 flush 到 参数列表 或 重定向目标
|
||||||
|
def _flush_token():
|
||||||
|
nonlocal \
|
||||||
|
current_string, \
|
||||||
|
current_string_raw, \
|
||||||
|
is_parsing_redirect_filename, \
|
||||||
|
current_redirect_target, \
|
||||||
|
has_token
|
||||||
|
if not has_token:
|
||||||
|
return
|
||||||
|
|
||||||
|
if is_parsing_redirect_filename:
|
||||||
|
current_redirect_target = current_string
|
||||||
|
is_parsing_redirect_filename = False # 重定向文件名只取一个 token
|
||||||
|
else:
|
||||||
|
current_command_args.append(current_string)
|
||||||
|
|
||||||
|
current_string = ""
|
||||||
|
current_string_raw = ""
|
||||||
|
has_token = False
|
||||||
|
|
||||||
|
# 辅助函数:将当前指令 flush 到当前组
|
||||||
|
def _flush_command() -> str | None:
|
||||||
|
nonlocal \
|
||||||
|
current_command_args, \
|
||||||
|
current_redirect_target, \
|
||||||
|
current_redirect_append
|
||||||
|
if not current_command_args:
|
||||||
|
return None
|
||||||
|
|
||||||
|
cmd_name = current_command_args[0]
|
||||||
|
args = current_command_args[1:]
|
||||||
|
|
||||||
|
matched = [
|
||||||
|
h for h in self.handlers if cmd_name in h.keywords or cmd_name == h.name
|
||||||
|
]
|
||||||
|
if not matched:
|
||||||
|
return f"不存在名为 {cmd_name} 的函数"
|
||||||
|
if len(matched) > 1:
|
||||||
|
logger.warning(
|
||||||
|
f"指令能对应超过一个文本处理器 CMD={cmd_name} handlers={self.handlers}"
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd = PipelineCommand(
|
||||||
|
handler=matched[0],
|
||||||
|
args=args,
|
||||||
|
redirect_target=current_redirect_target,
|
||||||
|
redirect_append=current_redirect_append,
|
||||||
|
)
|
||||||
|
current_group.append(cmd)
|
||||||
|
|
||||||
|
# 重置指令级状态
|
||||||
|
current_command_args = []
|
||||||
|
current_redirect_target = None
|
||||||
|
current_redirect_append = False
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 使用索引遍历以支持 look-ahead (处理 >>)
|
||||||
|
i = 0
|
||||||
|
length = len(script)
|
||||||
|
|
||||||
|
while i < length:
|
||||||
|
c = script[i]
|
||||||
|
|
||||||
|
match status:
|
||||||
|
case PipelineParseStatus.normal:
|
||||||
|
if c in whitespaces:
|
||||||
|
_flush_token()
|
||||||
|
|
||||||
|
elif c in "'\"":
|
||||||
|
status_in_string_pair = c
|
||||||
|
status = PipelineParseStatus.in_string
|
||||||
|
current_string_raw = ""
|
||||||
|
has_token = True
|
||||||
|
|
||||||
|
elif c == "|":
|
||||||
|
_flush_token()
|
||||||
|
if err := _flush_command():
|
||||||
|
return err
|
||||||
|
# 管道符不结束 group,继续在 current_group 添加
|
||||||
|
|
||||||
|
elif c == ";":
|
||||||
|
_flush_token()
|
||||||
|
if err := _flush_command():
|
||||||
|
return err
|
||||||
|
# 分号结束 group
|
||||||
|
if current_group:
|
||||||
|
pipeline.command_groups.append(current_group)
|
||||||
|
current_group = []
|
||||||
|
|
||||||
|
elif c == ">":
|
||||||
|
_flush_token() # 先结束之前的参数
|
||||||
|
# 检查是否是 append 模式 (>>)
|
||||||
|
if i + 1 < length and script[i + 1] == ">":
|
||||||
|
current_redirect_append = True
|
||||||
|
i += 1 # 跳过下一个 >
|
||||||
|
else:
|
||||||
|
current_redirect_append = False
|
||||||
|
|
||||||
|
# 标记下一个 token 为文件名
|
||||||
|
is_parsing_redirect_filename = True
|
||||||
|
|
||||||
|
else:
|
||||||
|
current_string += c
|
||||||
|
has_token = True
|
||||||
|
|
||||||
|
case PipelineParseStatus.in_string:
|
||||||
|
current_string_raw += c
|
||||||
|
if c == status_in_string_pair:
|
||||||
|
status = PipelineParseStatus.off_string
|
||||||
|
elif c == "\\":
|
||||||
|
status = PipelineParseStatus.in_string_to_escape
|
||||||
|
else:
|
||||||
|
current_string += c
|
||||||
|
|
||||||
|
case PipelineParseStatus.in_string_to_escape:
|
||||||
|
escape_map = {
|
||||||
|
"n": "\n",
|
||||||
|
"r": "\r",
|
||||||
|
"t": "\t",
|
||||||
|
"0": "\0",
|
||||||
|
"a": "\a",
|
||||||
|
"b": "\b",
|
||||||
|
"f": "\f",
|
||||||
|
"v": "\v",
|
||||||
|
"\\": "\\",
|
||||||
|
}
|
||||||
|
current_string += escape_map.get(c, c)
|
||||||
|
status = PipelineParseStatus.in_string
|
||||||
|
|
||||||
|
case PipelineParseStatus.off_string:
|
||||||
|
if c in whitespaces:
|
||||||
|
_flush_token()
|
||||||
|
status = PipelineParseStatus.normal
|
||||||
|
elif c == "|":
|
||||||
|
_flush_token()
|
||||||
|
if err := _flush_command():
|
||||||
|
return err
|
||||||
|
status = PipelineParseStatus.normal
|
||||||
|
elif c == ";":
|
||||||
|
_flush_token()
|
||||||
|
if err := _flush_command():
|
||||||
|
return err
|
||||||
|
if current_group:
|
||||||
|
pipeline.command_groups.append(current_group)
|
||||||
|
current_group = []
|
||||||
|
status = PipelineParseStatus.normal
|
||||||
|
elif c == ">":
|
||||||
|
_flush_token()
|
||||||
|
status = PipelineParseStatus.normal
|
||||||
|
# 回退索引,让下一次循环进入 normal 状态的 > 处理逻辑
|
||||||
|
i -= 1
|
||||||
|
else:
|
||||||
|
# 紧接着的字符继续作为当前字符串的一部分 (如 "abc"d)
|
||||||
|
current_string += c
|
||||||
|
current_string_raw = ""
|
||||||
|
status = PipelineParseStatus.normal
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# 循环结束后的收尾
|
||||||
|
_flush_token()
|
||||||
|
if err := _flush_command():
|
||||||
|
return err
|
||||||
|
|
||||||
|
if current_group:
|
||||||
|
pipeline.command_groups.append(current_group)
|
||||||
|
|
||||||
|
return pipeline
|
||||||
|
|
||||||
|
async def run_pipeline(
|
||||||
|
self,
|
||||||
|
pipeline: Pipeline,
|
||||||
|
istream: str | None,
|
||||||
|
env: TextHandlerEnvironment | None = None,
|
||||||
|
) -> list[TextHandleResult]:
|
||||||
|
if env is None:
|
||||||
|
env = TextHandlerEnvironment(is_trusted=False, buffers={})
|
||||||
|
|
||||||
|
results: list[TextHandleResult] = []
|
||||||
|
|
||||||
|
# 遍历执行指令组 (分号分隔),每个组独立产生输出
|
||||||
|
for group in pipeline.command_groups:
|
||||||
|
current_stream = istream
|
||||||
|
group_result = TextHandleResult(code=0, ostream=None)
|
||||||
|
|
||||||
|
# 遍历组内指令 (管道分隔)
|
||||||
|
for cmd in group:
|
||||||
|
try:
|
||||||
|
logger.debug(
|
||||||
|
f"Executing: {cmd.handler.name} args={cmd.args} redirect={cmd.redirect_target}"
|
||||||
|
)
|
||||||
|
result = await cmd.handler.handle(env, current_stream, cmd.args)
|
||||||
|
|
||||||
|
if result.code != 0:
|
||||||
|
# 组内出错,整条流水线中止
|
||||||
|
results.append(result)
|
||||||
|
return results
|
||||||
|
|
||||||
|
# 处理重定向逻辑
|
||||||
|
if cmd.redirect_target:
|
||||||
|
content_to_write = result.ostream or ""
|
||||||
|
target_buffer = cmd.redirect_target
|
||||||
|
|
||||||
|
if cmd.redirect_append:
|
||||||
|
old_content = env.buffers.get(target_buffer, "")
|
||||||
|
env.buffers[target_buffer] = old_content + content_to_write
|
||||||
|
else:
|
||||||
|
env.buffers[target_buffer] = content_to_write
|
||||||
|
|
||||||
|
current_stream = None
|
||||||
|
group_result = TextHandleResult(
|
||||||
|
code=0, ostream=None, attachment=result.attachment
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
current_stream = result.ostream
|
||||||
|
group_result = result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Pipeline execution failed at {cmd.handler.name}")
|
||||||
|
logger.exception(e)
|
||||||
|
results.append(
|
||||||
|
TextHandleResult(
|
||||||
|
code=-1, ostream="处理流水线时出现 python 错误"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
results.append(group_result)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def register_text_handlers(*handlers: TextHandler):
|
||||||
|
for handler in handlers:
|
||||||
|
PipelineRunner.get_runner().register(handler)
|
||||||
44
konabot/plugins/handle_text/handlers/ai_handlers.py
Normal file
44
konabot/plugins/handle_text/handlers/ai_handlers.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
from typing import Any, cast
|
||||||
|
from konabot.common.llm import get_llm
|
||||||
|
from konabot.plugins.handle_text.base import TextHandler, TextHandlerEnvironment, TextHandleResult
|
||||||
|
|
||||||
|
|
||||||
|
class THQwen(TextHandler):
|
||||||
|
name = "qwen"
|
||||||
|
|
||||||
|
async def handle(self, env: TextHandlerEnvironment, istream: str | None, args: list[str]) -> TextHandleResult:
|
||||||
|
llm = get_llm("qwen3-max")
|
||||||
|
messages = []
|
||||||
|
|
||||||
|
if istream is not None:
|
||||||
|
messages.append({
|
||||||
|
"role": "user",
|
||||||
|
"content": istream
|
||||||
|
})
|
||||||
|
if len(args) > 0:
|
||||||
|
message = ' '.join(args)
|
||||||
|
messages.append({
|
||||||
|
"role": "user",
|
||||||
|
"content": message,
|
||||||
|
})
|
||||||
|
if len(messages) == 0:
|
||||||
|
return TextHandleResult(
|
||||||
|
code=1,
|
||||||
|
ostream="使用方法:qwen <提示词>",
|
||||||
|
)
|
||||||
|
|
||||||
|
messages = [{
|
||||||
|
"role": "system",
|
||||||
|
"content": "除非用户要求,请尽可能短点回答。另外,当前环境不支持 Markdown 语法,如果可以,请使用纯文本回答"
|
||||||
|
}] + messages
|
||||||
|
result = await llm.chat(cast(Any, messages))
|
||||||
|
content = result.content
|
||||||
|
if content is None:
|
||||||
|
return TextHandleResult(
|
||||||
|
code=500,
|
||||||
|
ostream="问 AI 的时候发生了未知的错误",
|
||||||
|
)
|
||||||
|
return TextHandleResult(
|
||||||
|
code=0,
|
||||||
|
ostream=content,
|
||||||
|
)
|
||||||
346
konabot/plugins/handle_text/handlers/encoding_handlers.py
Normal file
346
konabot/plugins/handle_text/handlers/encoding_handlers.py
Normal file
@ -0,0 +1,346 @@
|
|||||||
|
import base64
|
||||||
|
from konabot.plugins.handle_text.base import (
|
||||||
|
TextHandleResult,
|
||||||
|
TextHandler,
|
||||||
|
TextHandlerEnvironment,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class THBase64(TextHandler):
|
||||||
|
name = "b64"
|
||||||
|
keywords = ["base64"]
|
||||||
|
|
||||||
|
async def handle(
|
||||||
|
self, env: TextHandlerEnvironment, istream: str | None, args: list[str]
|
||||||
|
) -> TextHandleResult:
|
||||||
|
# 用法: b64 encode/decode [encoding] [text]
|
||||||
|
if not args and istream is None:
|
||||||
|
return TextHandleResult(
|
||||||
|
1, "用法:b64 <encode|decode> [编码, 默认utf-8] [文本]"
|
||||||
|
)
|
||||||
|
|
||||||
|
mode = args[0].lower() if args else "encode"
|
||||||
|
encoding = args[1] if len(args) > 1 else "utf-8"
|
||||||
|
|
||||||
|
# 确定输入源
|
||||||
|
text = (
|
||||||
|
istream
|
||||||
|
if istream is not None
|
||||||
|
else (" ".join(args[2:]) if len(args) > 2 else "")
|
||||||
|
)
|
||||||
|
if not text:
|
||||||
|
return TextHandleResult(1, "输入文本为空")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if mode == "encode":
|
||||||
|
res = base64.b64encode(text.encode(encoding, "replace")).decode("ascii")
|
||||||
|
else:
|
||||||
|
res = base64.b64decode(text.encode("ascii")).decode(encoding, "replace")
|
||||||
|
return TextHandleResult(0, res)
|
||||||
|
except Exception as e:
|
||||||
|
return TextHandleResult(1, f"Base64 转换失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
class THCaesar(TextHandler):
|
||||||
|
name = "caesar"
|
||||||
|
keywords = ["凯撒", "rot"]
|
||||||
|
|
||||||
|
async def handle(
|
||||||
|
self, env: TextHandlerEnvironment, istream: str | None, args: list[str]
|
||||||
|
) -> TextHandleResult:
|
||||||
|
# 用法: caesar <shift> [text]
|
||||||
|
shift = int(args[0]) if args else 13
|
||||||
|
text = (
|
||||||
|
istream
|
||||||
|
if istream is not None
|
||||||
|
else (" ".join(args[1:]) if len(args) > 1 else "")
|
||||||
|
)
|
||||||
|
|
||||||
|
def _shift(char):
|
||||||
|
if not char.isalpha():
|
||||||
|
return char
|
||||||
|
start = ord("A") if char.isupper() else ord("a")
|
||||||
|
return chr((ord(char) - start + shift) % 26 + start)
|
||||||
|
|
||||||
|
res = "".join(_shift(c) for c in text)
|
||||||
|
return TextHandleResult(0, res)
|
||||||
|
|
||||||
|
|
||||||
|
class THReverse(TextHandler):
|
||||||
|
name = "reverse"
|
||||||
|
keywords = ["rev", "反转"]
|
||||||
|
|
||||||
|
async def handle(
|
||||||
|
self, env: TextHandlerEnvironment, istream: str | None, args: list[str]
|
||||||
|
) -> TextHandleResult:
|
||||||
|
text = istream if istream is not None else (" ".join(args) if args else "")
|
||||||
|
return TextHandleResult(0, text[::-1])
|
||||||
|
|
||||||
|
|
||||||
|
class THMorse(TextHandler):
|
||||||
|
name = "morse"
|
||||||
|
keywords = ["摩斯", "decode_morse"]
|
||||||
|
|
||||||
|
# 国际摩斯电码表 (部分)
|
||||||
|
MORSE_EN = {
|
||||||
|
".-": "A",
|
||||||
|
"-...": "B",
|
||||||
|
"-.-.": "C",
|
||||||
|
"-..": "D",
|
||||||
|
".": "E",
|
||||||
|
"..-.": "F",
|
||||||
|
"--.": "G",
|
||||||
|
"....": "H",
|
||||||
|
"..": "I",
|
||||||
|
".---": "J",
|
||||||
|
"-.-": "K",
|
||||||
|
".-..": "L",
|
||||||
|
"--": "M",
|
||||||
|
"-.": "N",
|
||||||
|
"---": "O",
|
||||||
|
".--.": "P",
|
||||||
|
"--.-": "Q",
|
||||||
|
".-.": "R",
|
||||||
|
"...": "S",
|
||||||
|
"-": "T",
|
||||||
|
"..-": "U",
|
||||||
|
"...-": "V",
|
||||||
|
".--": "W",
|
||||||
|
"-..-": "X",
|
||||||
|
"-.--": "Y",
|
||||||
|
"--..": "Z",
|
||||||
|
"-----": "0",
|
||||||
|
".----": "1",
|
||||||
|
"..---": "2",
|
||||||
|
"...--": "3",
|
||||||
|
"....-": "4",
|
||||||
|
".....": "5",
|
||||||
|
"-....": "6",
|
||||||
|
"--...": "7",
|
||||||
|
"---..": "8",
|
||||||
|
"----.": "9",
|
||||||
|
"/": " ",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 日文和文摩斯电码表 (Wabun Code)
|
||||||
|
MORSE_JP = {
|
||||||
|
"--.--": "ア",
|
||||||
|
".-": "イ",
|
||||||
|
"..-": "ウ",
|
||||||
|
"-.---": "エ",
|
||||||
|
".-...": "オ",
|
||||||
|
".-..": "カ",
|
||||||
|
"-.-..": "キ",
|
||||||
|
"...-": "ク",
|
||||||
|
"-.--": "ケ",
|
||||||
|
"----": "コ",
|
||||||
|
"-.-.-": "サ",
|
||||||
|
"--.-.": "シ",
|
||||||
|
"---.-": "ス",
|
||||||
|
".---.": "セ",
|
||||||
|
"---.": "ソ",
|
||||||
|
"-.": "タ",
|
||||||
|
"..-.": "チ",
|
||||||
|
".--.": "ツ",
|
||||||
|
".-.--": "テ",
|
||||||
|
"..-..": "ト",
|
||||||
|
".-.": "ナ",
|
||||||
|
"-.-.": "ニ",
|
||||||
|
"....": "ヌ",
|
||||||
|
"--.-": "ネ",
|
||||||
|
"..--": "ノ",
|
||||||
|
"-...": "ハ",
|
||||||
|
"--..-": "ヒ",
|
||||||
|
"--..": "フ",
|
||||||
|
".": "ヘ",
|
||||||
|
"-..": "ホ",
|
||||||
|
"-..-": "マ",
|
||||||
|
"..-.-": "ミ",
|
||||||
|
"-": "ム",
|
||||||
|
"-...-": "メ",
|
||||||
|
"-..-.": "モ",
|
||||||
|
".--": "ヤ",
|
||||||
|
"-..--": "ユ",
|
||||||
|
"--": "ヨ",
|
||||||
|
"...": "ラ",
|
||||||
|
"--.": "リ",
|
||||||
|
"-.--.": "ル",
|
||||||
|
"---": "レ",
|
||||||
|
".-.-": "ロ",
|
||||||
|
"-.-": "ワ",
|
||||||
|
".-..-": "ヰ",
|
||||||
|
".--..": "ヱ",
|
||||||
|
".---": "ヲ",
|
||||||
|
".-.-.": "ン",
|
||||||
|
"-..-.--.": "ッ",
|
||||||
|
"-..-.--": "ャ",
|
||||||
|
"-..--..--": "ュ",
|
||||||
|
"-..---": "ョ",
|
||||||
|
"-..---.--": "ァ",
|
||||||
|
"-..-.-": "ィ",
|
||||||
|
"-..-..-": "ゥ",
|
||||||
|
"-..--.---": "ェ",
|
||||||
|
"-..-.-...": "ォ",
|
||||||
|
"-..-.-..": "ヵ",
|
||||||
|
"-..--.--": "ヶ",
|
||||||
|
"..": "゛",
|
||||||
|
"..--.": "゜",
|
||||||
|
".--.-": "ー",
|
||||||
|
".-.-.-": "、",
|
||||||
|
".-.-..": "。",
|
||||||
|
"-.--.-": "(",
|
||||||
|
".-..-.": ")",
|
||||||
|
}
|
||||||
|
|
||||||
|
async def handle(
|
||||||
|
self, env: TextHandlerEnvironment, istream: str | None, args: list[str]
|
||||||
|
) -> TextHandleResult:
|
||||||
|
"""
|
||||||
|
用法: morse <mode: en|jp> [text]
|
||||||
|
例子: morse en .... . .-.. .-.. ---
|
||||||
|
"""
|
||||||
|
if not args and istream is None:
|
||||||
|
return TextHandleResult(
|
||||||
|
1, "用法:morse <en|jp> <电码>。使用空格分隔字符,/ 分隔单词。"
|
||||||
|
)
|
||||||
|
|
||||||
|
mode = args[0].lower() if args else "en"
|
||||||
|
text = (
|
||||||
|
istream
|
||||||
|
if istream is not None
|
||||||
|
else (" ".join(args[1:]) if len(args) > 1 else "")
|
||||||
|
)
|
||||||
|
|
||||||
|
if not text:
|
||||||
|
return TextHandleResult(1, "请输入电码内容")
|
||||||
|
|
||||||
|
# 选择词典
|
||||||
|
mapping = self.MORSE_JP if mode == "jp" else self.MORSE_EN
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 按空格切分符号,过滤掉多余空位
|
||||||
|
tokens = [t for t in text.split(" ") if t]
|
||||||
|
decoded = []
|
||||||
|
|
||||||
|
for token in tokens:
|
||||||
|
# 处理部分解谜中可能出现的换行或特殊斜杠
|
||||||
|
token = token.strip()
|
||||||
|
if token in mapping:
|
||||||
|
decoded.append(mapping[token])
|
||||||
|
else:
|
||||||
|
decoded.append("[?]") # 无法识别的符号
|
||||||
|
|
||||||
|
return TextHandleResult(0, "".join(decoded))
|
||||||
|
except Exception as e:
|
||||||
|
return TextHandleResult(1, f"摩斯电码解析出错: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
class THBaseConv(TextHandler):
|
||||||
|
name = "baseconv"
|
||||||
|
keywords = ["进制转换"]
|
||||||
|
|
||||||
|
async def handle(self, env: TextHandlerEnvironment, istream: str | None, args: list[str]) -> TextHandleResult:
|
||||||
|
# 用法: baseconv <src_base> <dst_base> [text]
|
||||||
|
if len(args) < 2 and istream is None:
|
||||||
|
return TextHandleResult(1, "用法:baseconv <原进制> <目标进制> [文本]")
|
||||||
|
|
||||||
|
src_base = int(args[0])
|
||||||
|
dst_base = int(args[1])
|
||||||
|
val_str = istream if istream is not None else "".join(args[2:])
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 先转为 10 进制中间量,再转为目标进制
|
||||||
|
decimal_val = int(val_str, src_base)
|
||||||
|
|
||||||
|
if dst_base == 10:
|
||||||
|
res = str(decimal_val)
|
||||||
|
elif dst_base == 16:
|
||||||
|
res = hex(decimal_val)[2:]
|
||||||
|
else:
|
||||||
|
# 通用任意进制转换逻辑
|
||||||
|
chars = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||||
|
res = ""
|
||||||
|
temp = decimal_val
|
||||||
|
while temp > 0:
|
||||||
|
res = chars[temp % dst_base] + res
|
||||||
|
temp //= dst_base
|
||||||
|
res = res or "0"
|
||||||
|
|
||||||
|
return TextHandleResult(0, res.upper() if dst_base == 16 else res)
|
||||||
|
except Exception as e:
|
||||||
|
return TextHandleResult(1, f"转换失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
class THAlphaConv(TextHandler):
|
||||||
|
name = "alphaconv"
|
||||||
|
keywords = ["字母表转换"]
|
||||||
|
|
||||||
|
async def handle(self, env: TextHandlerEnvironment, istream: str | None, args: list[str]) -> TextHandleResult:
|
||||||
|
# 用法: alphaconv <alphabet> <to_hex|from_hex> [text]
|
||||||
|
if len(args) < 2:
|
||||||
|
return TextHandleResult(1, "用法:alphaconv <字母表> <to_hex|from_hex> [文本]")
|
||||||
|
|
||||||
|
alphabet = args[0]
|
||||||
|
mode = args[1].lower()
|
||||||
|
base = len(alphabet)
|
||||||
|
text = istream if istream is not None else "".join(args[2:])
|
||||||
|
|
||||||
|
try:
|
||||||
|
if mode == "to_hex":
|
||||||
|
# 自定义字母表 -> 10进制 -> 16进制
|
||||||
|
val = 0
|
||||||
|
for char in text:
|
||||||
|
val = val * base + alphabet.index(char)
|
||||||
|
return TextHandleResult(0, hex(val)[2:])
|
||||||
|
else:
|
||||||
|
# 16进制 -> 10进制 -> 自定义字母表
|
||||||
|
val = int(text, 16)
|
||||||
|
res = ""
|
||||||
|
while val > 0:
|
||||||
|
res = alphabet[val % base] + res
|
||||||
|
val //= base
|
||||||
|
return TextHandleResult(0, res or alphabet[0])
|
||||||
|
except Exception as e:
|
||||||
|
return TextHandleResult(1, f"字母表转换失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
class THB64Hex(TextHandler):
|
||||||
|
name = "b64hex"
|
||||||
|
|
||||||
|
async def handle(self, env: TextHandlerEnvironment, istream: str | None, args: list[str]) -> TextHandleResult:
|
||||||
|
# 用法: b64hex <enc|dec> [text]
|
||||||
|
mode = args[0] if args else "dec"
|
||||||
|
text = istream if istream is not None else "".join(args[1:])
|
||||||
|
|
||||||
|
try:
|
||||||
|
if mode == "enc": # Hex -> B64
|
||||||
|
raw_bytes = bytes.fromhex(text)
|
||||||
|
res = base64.b64encode(raw_bytes).decode()
|
||||||
|
else: # B64 -> Hex
|
||||||
|
raw_bytes = base64.b64decode(text)
|
||||||
|
res = raw_bytes.hex()
|
||||||
|
return TextHandleResult(0, res)
|
||||||
|
except Exception as e:
|
||||||
|
return TextHandleResult(1, f"Base64-Hex 转换失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
class THAlign(TextHandler):
|
||||||
|
name = "align"
|
||||||
|
keywords = ["format", "排版"]
|
||||||
|
|
||||||
|
async def handle(self, env: TextHandlerEnvironment, istream: str | None, args: list[str]) -> TextHandleResult:
|
||||||
|
# 用法: align <n:每组长度> <m:每行组数> [text]
|
||||||
|
# 例子: align 2 8 (即 2个一组,8组一行,类似 0011 2233...)
|
||||||
|
n = int(args[0]) if len(args) > 0 else 2
|
||||||
|
m = int(args[1]) if len(args) > 1 else 8
|
||||||
|
text = istream if istream is not None else "".join(args[2:])
|
||||||
|
|
||||||
|
# 移除现有空格换行以便重新排版
|
||||||
|
text = "".join(text.split())
|
||||||
|
|
||||||
|
chunks = [text[i:i+n] for i in range(0, len(text), n)]
|
||||||
|
lines = []
|
||||||
|
for i in range(0, len(chunks), m):
|
||||||
|
lines.append(" ".join(chunks[i:i+m]))
|
||||||
|
|
||||||
|
return TextHandleResult(0, "\n".join(lines))
|
||||||
37
konabot/plugins/handle_text/handlers/random_handlers.py
Normal file
37
konabot/plugins/handle_text/handlers/random_handlers.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import random
|
||||||
|
from konabot.plugins.handle_text.base import TextHandleResult, TextHandler, TextHandlerEnvironment, TextHandlerSync
|
||||||
|
|
||||||
|
|
||||||
|
class THShuffle(TextHandler):
|
||||||
|
name: str = "shuffle"
|
||||||
|
keywords: list = ["打乱"]
|
||||||
|
|
||||||
|
async def handle(self, env: TextHandlerEnvironment, istream: str | None, args: list[str]) -> TextHandleResult:
|
||||||
|
if istream is not None:
|
||||||
|
w = istream
|
||||||
|
elif len(args) == 0:
|
||||||
|
return TextHandleResult(1, "使用方法:打乱 <待打乱的文本>,或者使用管道符传入待打乱的文本")
|
||||||
|
else:
|
||||||
|
w = args[0]
|
||||||
|
args = args[1:]
|
||||||
|
|
||||||
|
w = [*w]
|
||||||
|
random.shuffle(w)
|
||||||
|
return TextHandleResult(0, ''.join(w))
|
||||||
|
|
||||||
|
|
||||||
|
class THSorted(TextHandlerSync):
|
||||||
|
name = "sort"
|
||||||
|
keywords = ["排序"]
|
||||||
|
|
||||||
|
def handle_sync(self, env: TextHandlerEnvironment, istream: str | None, args: list[str]) -> TextHandleResult:
|
||||||
|
if istream is not None:
|
||||||
|
w = istream
|
||||||
|
elif len(args) == 0:
|
||||||
|
return TextHandleResult(1, "使用方法:排序 <待排序的文本>,或者使用管道符传入待打乱的文本")
|
||||||
|
else:
|
||||||
|
w = args[0]
|
||||||
|
args = args[1:]
|
||||||
|
|
||||||
|
return TextHandleResult(0, ''.join(sorted([*w])))
|
||||||
|
|
||||||
92
konabot/plugins/handle_text/handlers/unix_handlers.py
Normal file
92
konabot/plugins/handle_text/handlers/unix_handlers.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from konabot.plugins.handle_text.base import (
|
||||||
|
TextHandleResult,
|
||||||
|
TextHandler,
|
||||||
|
TextHandlerEnvironment,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class THEcho(TextHandler):
|
||||||
|
name = "echo"
|
||||||
|
|
||||||
|
async def handle(
|
||||||
|
self, env: TextHandlerEnvironment, istream: str | None, args: list[str]
|
||||||
|
) -> TextHandleResult:
|
||||||
|
if len(args) == 0 and istream is None:
|
||||||
|
return TextHandleResult(1, "请在 echo 后面添加需要输出的文本")
|
||||||
|
if istream is not None:
|
||||||
|
return TextHandleResult(0, "\n".join([istream] + args))
|
||||||
|
return TextHandleResult(0, "\n".join(args))
|
||||||
|
|
||||||
|
|
||||||
|
class THCat(TextHandler):
|
||||||
|
name = "cat"
|
||||||
|
|
||||||
|
async def handle(
|
||||||
|
self, env: TextHandlerEnvironment, istream: str | None, args: list[str]
|
||||||
|
) -> TextHandleResult:
|
||||||
|
# No args: pass through stdin (like Unix cat with no arguments)
|
||||||
|
if len(args) == 0:
|
||||||
|
if istream is None:
|
||||||
|
return TextHandleResult(
|
||||||
|
1,
|
||||||
|
"cat 使用方法:cat [缓存名 ...]\n使用 - 代表标准输入,可拼接多个缓存",
|
||||||
|
)
|
||||||
|
return TextHandleResult(0, istream)
|
||||||
|
|
||||||
|
# Concatenate all specified sources in order
|
||||||
|
parts: list[str] = []
|
||||||
|
for arg in args:
|
||||||
|
if arg == "-":
|
||||||
|
if istream is None:
|
||||||
|
return TextHandleResult(2, "标准输入为空(没有管道输入或回复消息)")
|
||||||
|
parts.append(istream)
|
||||||
|
else:
|
||||||
|
if arg not in env.buffers:
|
||||||
|
return TextHandleResult(2, f"缓存 {arg} 不存在")
|
||||||
|
parts.append(env.buffers[arg])
|
||||||
|
|
||||||
|
return TextHandleResult(0, "\n".join(parts))
|
||||||
|
|
||||||
|
|
||||||
|
class THRm(TextHandler):
|
||||||
|
name = "rm"
|
||||||
|
|
||||||
|
async def handle(
|
||||||
|
self, env: TextHandlerEnvironment, istream: str | None, args: list[str]
|
||||||
|
) -> TextHandleResult:
|
||||||
|
if len(args) != 1:
|
||||||
|
return TextHandleResult(1, "rm 使用方法:rm <缓存名>")
|
||||||
|
buf = args[0]
|
||||||
|
if buf == "-":
|
||||||
|
buf = istream
|
||||||
|
if buf not in env.buffers:
|
||||||
|
return TextHandleResult(2, f"缓存 {buf} 不存在")
|
||||||
|
del env.buffers[buf]
|
||||||
|
return TextHandleResult(0, None)
|
||||||
|
|
||||||
|
|
||||||
|
class THReplace(TextHandler):
|
||||||
|
name = "replace"
|
||||||
|
keywords = ["sed", "替换"]
|
||||||
|
|
||||||
|
async def handle(
|
||||||
|
self, env: TextHandlerEnvironment, istream: str | None, args: list[str]
|
||||||
|
) -> TextHandleResult:
|
||||||
|
# 用法: replace <pattern> <replacement> [text]
|
||||||
|
if len(args) < 2:
|
||||||
|
return TextHandleResult(1, "用法:replace <正则> <替换内容> [文本]")
|
||||||
|
|
||||||
|
pattern, repl = args[0], args[1]
|
||||||
|
text = (
|
||||||
|
istream
|
||||||
|
if istream is not None
|
||||||
|
else (" ".join(args[2:]) if len(args) > 2 else "")
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
res = re.sub(pattern, repl, text)
|
||||||
|
return TextHandleResult(0, res)
|
||||||
|
except Exception as e:
|
||||||
|
return TextHandleResult(1, f"正则错误: {str(e)}")
|
||||||
126
konabot/plugins/handle_text/handlers/whitespace_handlers.py
Normal file
126
konabot/plugins/handle_text/handlers/whitespace_handlers.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from konabot.plugins.handle_text.base import (
|
||||||
|
TextHandleResult,
|
||||||
|
TextHandler,
|
||||||
|
TextHandlerEnvironment,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_text(istream: str | None, args: list[str]) -> str | None:
|
||||||
|
"""从 istream 或 args 中获取待处理文本"""
|
||||||
|
if istream is not None:
|
||||||
|
return istream
|
||||||
|
if args:
|
||||||
|
return " ".join(args)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class THTrim(TextHandler):
|
||||||
|
name = "trim"
|
||||||
|
keywords = ["strip", "去空格"]
|
||||||
|
|
||||||
|
async def handle(
|
||||||
|
self, env: TextHandlerEnvironment, istream: str | None, args: list[str]
|
||||||
|
) -> TextHandleResult:
|
||||||
|
text = _get_text(istream, args)
|
||||||
|
if text is None:
|
||||||
|
return TextHandleResult(1, "trim 使用方法:trim [文本]\n去除首尾空白字符")
|
||||||
|
return TextHandleResult(0, text.strip())
|
||||||
|
|
||||||
|
|
||||||
|
class THLTrim(TextHandler):
|
||||||
|
name = "ltrim"
|
||||||
|
keywords = ["lstrip"]
|
||||||
|
|
||||||
|
async def handle(
|
||||||
|
self, env: TextHandlerEnvironment, istream: str | None, args: list[str]
|
||||||
|
) -> TextHandleResult:
|
||||||
|
text = _get_text(istream, args)
|
||||||
|
if text is None:
|
||||||
|
return TextHandleResult(1, "ltrim 使用方法:ltrim [文本]\n去除左侧空白字符")
|
||||||
|
return TextHandleResult(0, text.lstrip())
|
||||||
|
|
||||||
|
|
||||||
|
class THRTrim(TextHandler):
|
||||||
|
name = "rtrim"
|
||||||
|
keywords = ["rstrip"]
|
||||||
|
|
||||||
|
async def handle(
|
||||||
|
self, env: TextHandlerEnvironment, istream: str | None, args: list[str]
|
||||||
|
) -> TextHandleResult:
|
||||||
|
text = _get_text(istream, args)
|
||||||
|
if text is None:
|
||||||
|
return TextHandleResult(1, "rtrim 使用方法:rtrim [文本]\n去除右侧空白字符")
|
||||||
|
return TextHandleResult(0, text.rstrip())
|
||||||
|
|
||||||
|
|
||||||
|
class THSqueeze(TextHandler):
|
||||||
|
name = "squeeze"
|
||||||
|
keywords = ["压缩空白"]
|
||||||
|
|
||||||
|
async def handle(
|
||||||
|
self, env: TextHandlerEnvironment, istream: str | None, args: list[str]
|
||||||
|
) -> TextHandleResult:
|
||||||
|
text = _get_text(istream, args)
|
||||||
|
if text is None:
|
||||||
|
return TextHandleResult(
|
||||||
|
1, "squeeze 使用方法:squeeze [文本]\n将连续空白字符压缩为单个空格"
|
||||||
|
)
|
||||||
|
return TextHandleResult(0, re.sub(r"[ \t]+", " ", text))
|
||||||
|
|
||||||
|
|
||||||
|
class THLines(TextHandler):
|
||||||
|
name = "lines"
|
||||||
|
keywords = ["行处理"]
|
||||||
|
|
||||||
|
async def handle(
|
||||||
|
self, env: TextHandlerEnvironment, istream: str | None, args: list[str]
|
||||||
|
) -> TextHandleResult:
|
||||||
|
# lines <子命令> [文本]
|
||||||
|
# 子命令: trim | empty | squeeze
|
||||||
|
if len(args) < 1:
|
||||||
|
return TextHandleResult(
|
||||||
|
1,
|
||||||
|
"lines 使用方法:lines <子命令> [文本]\n"
|
||||||
|
"子命令:\n"
|
||||||
|
" trim - 去除每行首尾空白\n"
|
||||||
|
" empty - 去除所有空行\n"
|
||||||
|
" squeeze - 将连续空行压缩为一行",
|
||||||
|
)
|
||||||
|
|
||||||
|
subcmd = args[0]
|
||||||
|
text = (
|
||||||
|
istream
|
||||||
|
if istream is not None
|
||||||
|
else (" ".join(args[1:]) if len(args) > 1 else None)
|
||||||
|
)
|
||||||
|
if text is None:
|
||||||
|
return TextHandleResult(1, "请提供需要处理的文本(通过管道或参数)")
|
||||||
|
|
||||||
|
raw_lines = text.split("\n")
|
||||||
|
|
||||||
|
match subcmd:
|
||||||
|
case "trim":
|
||||||
|
result = "\n".join(line.strip() for line in raw_lines)
|
||||||
|
case "empty":
|
||||||
|
result = "\n".join(line for line in raw_lines if line.strip())
|
||||||
|
case "squeeze":
|
||||||
|
squeezed: list[str] = []
|
||||||
|
prev_empty = False
|
||||||
|
for line in raw_lines:
|
||||||
|
is_empty = not line.strip()
|
||||||
|
if is_empty:
|
||||||
|
if not prev_empty:
|
||||||
|
squeezed.append("")
|
||||||
|
prev_empty = True
|
||||||
|
else:
|
||||||
|
squeezed.append(line)
|
||||||
|
prev_empty = False
|
||||||
|
result = "\n".join(squeezed)
|
||||||
|
case _:
|
||||||
|
return TextHandleResult(
|
||||||
|
1, f"未知子命令:{subcmd}\n可用:trim, empty, squeeze"
|
||||||
|
)
|
||||||
|
|
||||||
|
return TextHandleResult(0, result)
|
||||||
@ -2,7 +2,6 @@ import random
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
import opencc
|
import opencc
|
||||||
|
|
||||||
from nonebot import on_message
|
|
||||||
from nonebot.adapters import Event as BaseEvent
|
from nonebot.adapters import Event as BaseEvent
|
||||||
from nonebot.adapters.discord.event import MessageEvent as DiscordMessageEvent
|
from nonebot.adapters.discord.event import MessageEvent as DiscordMessageEvent
|
||||||
from nonebot_plugin_alconna import (
|
from nonebot_plugin_alconna import (
|
||||||
@ -13,6 +12,10 @@ from nonebot_plugin_alconna import (
|
|||||||
on_alconna,
|
on_alconna,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from konabot.common.web_render import konaweb
|
||||||
|
from konabot.common.web_render.core import WebRenderer
|
||||||
|
from konabot.plugins.hanzi.er_data import ErFontData
|
||||||
|
|
||||||
convert_type = ["简","簡","繁","正","港","日"]
|
convert_type = ["简","簡","繁","正","港","日"]
|
||||||
|
|
||||||
compiled_str = "|".join([f"{a}{mid}{b}" for mid in ["转","轉","転"] for a in convert_type for b in convert_type if a != b])
|
compiled_str = "|".join([f"{a}{mid}{b}" for mid in ["转","轉","転"] for a in convert_type for b in convert_type if a != b])
|
||||||
@ -25,6 +28,7 @@ def hanzi_to_abbr(hanzi: str) -> str:
|
|||||||
"正": "t",
|
"正": "t",
|
||||||
"港": "hk",
|
"港": "hk",
|
||||||
"日": "jp",
|
"日": "jp",
|
||||||
|
"二": "er",
|
||||||
}
|
}
|
||||||
return mapping.get(hanzi, "")
|
return mapping.get(hanzi, "")
|
||||||
|
|
||||||
@ -35,6 +39,9 @@ def check_valid_convert_type(convert_type: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def convert(source, src_abbr, dst_abbr):
|
def convert(source, src_abbr, dst_abbr):
|
||||||
|
if dst_abbr == "er":
|
||||||
|
# 直接转换为二简
|
||||||
|
return ErFontData.convert_text(source)
|
||||||
convert_type_key = f"{src_abbr}2{dst_abbr}"
|
convert_type_key = f"{src_abbr}2{dst_abbr}"
|
||||||
if not check_valid_convert_type(convert_type_key):
|
if not check_valid_convert_type(convert_type_key):
|
||||||
# 先转为繁体,再转为目标
|
# 先转为繁体,再转为目标
|
||||||
@ -98,12 +105,11 @@ async def _(msg: UniMsg, event: BaseEvent, source: Optional[str] = None):
|
|||||||
converted = convert(to_convert, src_abbr, dst_abbr)
|
converted = convert(to_convert, src_abbr, dst_abbr)
|
||||||
|
|
||||||
converted_prefix = convert("转换结果", "s", dst_abbr)
|
converted_prefix = convert("转换结果", "s", dst_abbr)
|
||||||
|
|
||||||
await evt.send(await UniMessage().text(f"{converted_prefix}:{converted}").export())
|
await evt.send(await UniMessage().text(f"{converted_prefix}:{converted}").export())
|
||||||
|
|
||||||
shuo = ["说","說"]
|
shuo = ["说","說"]
|
||||||
|
|
||||||
full_name_type = ["简体","簡體","繁體","繁体","正體","正体","港話","港话","日文"]
|
full_name_type = ["简体","簡體","繁體","繁体","正體","正体","港話","港话","日文","二简","二簡"]
|
||||||
|
|
||||||
combined_list = [f"{a}{b}" for a in shuo for b in full_name_type]
|
combined_list = [f"{a}{b}" for a in shuo for b in full_name_type]
|
||||||
|
|
||||||
@ -151,20 +157,47 @@ async def _(msg: UniMsg, event: BaseEvent, source: Optional[str] = None):
|
|||||||
dst = "港"
|
dst = "港"
|
||||||
case "說日文" | "说日文":
|
case "說日文" | "说日文":
|
||||||
dst = "日"
|
dst = "日"
|
||||||
|
case "說二簡" | "说二简" | "說二簡" | "说二簡":
|
||||||
|
dst = "二"
|
||||||
dst_abbr = hanzi_to_abbr(dst)
|
dst_abbr = hanzi_to_abbr(dst)
|
||||||
if not dst_abbr:
|
if not dst_abbr:
|
||||||
notice = "不支持的转换类型,请使用“简体”、“繁體”、“正體”、“港話”、“日文”等。"
|
notice = "不支持的转换类型,请使用“简体”、“繁體”、“正體”、“港話”、“日文”、“二简”等。"
|
||||||
await evt.send(await UniMessage().text(notice).export())
|
await evt.send(await UniMessage().text(notice).export())
|
||||||
return
|
return
|
||||||
# 循环,将源语言一次次转换为目标语言
|
# 如果是二简,直接转换
|
||||||
current_text = to_convert
|
if dst_abbr == "er":
|
||||||
for src_abbr in ["s","hk","jp","tw","t"]:
|
current_text = ErFontData.convert_text(to_convert)
|
||||||
if src_abbr != dst_abbr:
|
else:
|
||||||
current_text = convert(current_text, src_abbr, dst_abbr)
|
# 循环,将源语言一次次转换为目标语言
|
||||||
|
current_text = to_convert
|
||||||
|
for src_abbr in ["s","hk","jp","tw","t"]:
|
||||||
|
if src_abbr != dst_abbr:
|
||||||
|
current_text = convert(current_text, src_abbr, dst_abbr)
|
||||||
|
|
||||||
converted_prefix = convert("转换结果", "s", dst_abbr)
|
converted_prefix = convert("转换结果", "s", dst_abbr)
|
||||||
|
|
||||||
await evt.send(await UniMessage().text(f"{converted_prefix}:{current_text}").export())
|
if "span" in current_text:
|
||||||
|
# 改为网页渲染
|
||||||
|
render_result = await render_with_web_renderer(current_text)
|
||||||
|
await evt.send(await UniMessage().image(raw=render_result).export())
|
||||||
|
else:
|
||||||
|
await evt.send(await UniMessage().text(f"{converted_prefix}:{current_text}").export())
|
||||||
|
|
||||||
|
async def render_with_web_renderer(text: str) -> bytes:
|
||||||
|
async def page_function(page):
|
||||||
|
# 找到id为content的文本框
|
||||||
|
await page.wait_for_selector('textarea[name=content]')
|
||||||
|
# 填入文本
|
||||||
|
await page.locator('textarea[name=content]').fill(text)
|
||||||
|
|
||||||
|
out = await WebRenderer.render_with_persistent_page(
|
||||||
|
"markdown_renderer",
|
||||||
|
konaweb('old_font'),
|
||||||
|
target='#main',
|
||||||
|
other_function=page_function,
|
||||||
|
)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
def random_char(char: str) -> str:
|
def random_char(char: str) -> str:
|
||||||
dst_abbr = random.choice(["s","t","hk","jp","tw"])
|
dst_abbr = random.choice(["s","t","hk","jp","tw"])
|
||||||
@ -214,4 +247,19 @@ async def _(msg: UniMsg, event: BaseEvent, source: Optional[str] = None):
|
|||||||
final_text = random_string(to_convert)
|
final_text = random_string(to_convert)
|
||||||
converted_prefix = convert(random_string("转换结果"), "s", "s")
|
converted_prefix = convert(random_string("转换结果"), "s", "s")
|
||||||
|
|
||||||
await evt.send(await UniMessage().text(f"{converted_prefix}:{final_text}").export())
|
await evt.send(await UniMessage().text(f"{converted_prefix}:{final_text}").export())
|
||||||
|
|
||||||
|
def get_char(char: str, abbr: str) -> str:
|
||||||
|
output = ""
|
||||||
|
for src_abbr in ["s","hk","jp","tw","t"]:
|
||||||
|
if src_abbr != abbr:
|
||||||
|
output += convert(char, src_abbr, abbr)
|
||||||
|
return output
|
||||||
|
|
||||||
|
def get_all_variants(char: str) -> str:
|
||||||
|
output = ""
|
||||||
|
for abbr in ["s","hk","jp","tw","t"]:
|
||||||
|
for src_abbr in ["s","hk","jp","tw","t"]:
|
||||||
|
if src_abbr != abbr:
|
||||||
|
output += convert(char, src_abbr, abbr)
|
||||||
|
return output
|
||||||
45
konabot/plugins/hanzi/er_data.py
Normal file
45
konabot/plugins/hanzi/er_data.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import csv
|
||||||
|
from nonebot import logger
|
||||||
|
from nonebot_plugin_apscheduler import driver
|
||||||
|
from konabot.common.path import ASSETS_PATH
|
||||||
|
|
||||||
|
FONT_ASSETS_PATH = ASSETS_PATH / "old_font"
|
||||||
|
|
||||||
|
class ErFontData:
|
||||||
|
data = {}
|
||||||
|
temp_featured_fonts = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def init(cls):
|
||||||
|
logger.info("加载二简字体数据...")
|
||||||
|
path = FONT_ASSETS_PATH / "symtable.csv"
|
||||||
|
if not path.exists():
|
||||||
|
return
|
||||||
|
with open(path, "r", encoding="utf-8-sig") as f:
|
||||||
|
reader = csv.DictReader(f)
|
||||||
|
for row in reader:
|
||||||
|
if len(row["ss05"]) > 0:
|
||||||
|
cls.data[row["trad"]] = {"char": row["ss05"][0], "type": "ss05", "render": False}
|
||||||
|
if "er" in row["ss05"]:
|
||||||
|
cls.data[row["trad"]]["render"] = True
|
||||||
|
elif len(row["ss06"]) > 0:
|
||||||
|
cls.data[row["trad"]] = {"char": row["ss06"][0], "type": "ss06", "render": False}
|
||||||
|
if "er" in row["ss06"]:
|
||||||
|
cls.data[row["trad"]]["render"] = True
|
||||||
|
logger.info(f"二简字体数据加载完成,包含 {len(cls.data)} 个字。")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, char: str) -> str:
|
||||||
|
if char not in cls.data:
|
||||||
|
return char
|
||||||
|
if cls.data[char]["render"]:
|
||||||
|
return f"<span class={cls.data[char]['type']}>{cls.data[char]['char']}</span>"
|
||||||
|
return cls.data[char]["char"]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_text(cls, text: str) -> str:
|
||||||
|
return "".join([cls.get(c) for c in text])
|
||||||
|
|
||||||
|
@driver.on_startup
|
||||||
|
async def load_er_font_data():
|
||||||
|
ErFontData.init()
|
||||||
@ -1,11 +1,12 @@
|
|||||||
import asyncio as asynkio
|
import asyncio as asynkio
|
||||||
import datetime
|
import datetime
|
||||||
|
from io import BytesIO
|
||||||
import json
|
import json
|
||||||
import secrets
|
import secrets
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from PIL import Image
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from nonebot import on_message
|
from nonebot import on_message
|
||||||
import nonebot
|
import nonebot
|
||||||
@ -617,14 +618,23 @@ async def _(event: BaseEvent, target: DepLongTaskTarget):
|
|||||||
# 打开好吧狗本地文件
|
# 打开好吧狗本地文件
|
||||||
with open(ASSETS_PATH / "img" / "dog" / "haoba_dog.jpg", "rb") as f:
|
with open(ASSETS_PATH / "img" / "dog" / "haoba_dog.jpg", "rb") as f:
|
||||||
img_data = f.read()
|
img_data = f.read()
|
||||||
|
# 把好吧狗变成 GIF 格式以缩小尺寸
|
||||||
|
img_data = await convert_image_to_gif(img_data)
|
||||||
await evt.send(await UniMessage().image(raw=img_data).export())
|
await evt.send(await UniMessage().image(raw=img_data).export())
|
||||||
await end_game(event, group_id)
|
await end_game(event, group_id)
|
||||||
else:
|
else:
|
||||||
await evt.send(
|
# await evt.send(
|
||||||
await UniMessage().text("当前没有成语接龙游戏在进行中!").export()
|
# await UniMessage().text("当前没有成语接龙游戏在进行中!").export()
|
||||||
)
|
# )
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
async def convert_image_to_gif(image_data: bytes) -> bytes:
|
||||||
|
with Image.open(BytesIO(image_data)) as img:
|
||||||
|
with BytesIO() as output:
|
||||||
|
img.save(output, format="GIF")
|
||||||
|
return output.getvalue()
|
||||||
|
|
||||||
# 跳过
|
# 跳过
|
||||||
evt = on_alconna(
|
evt = on_alconna(
|
||||||
Alconna("跳过成语"), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=True
|
Alconna("跳过成语"), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=True
|
||||||
@ -642,6 +652,8 @@ async def _(target: DepLongTaskTarget):
|
|||||||
# 发送哈哈狗图片
|
# 发送哈哈狗图片
|
||||||
with open(ASSETS_PATH / "img" / "dog" / "haha_dog.jpg", "rb") as f:
|
with open(ASSETS_PATH / "img" / "dog" / "haha_dog.jpg", "rb") as f:
|
||||||
img_data = f.read()
|
img_data = f.read()
|
||||||
|
# 把哈哈狗变成 GIF 格式以缩小尺寸
|
||||||
|
img_data = await convert_image_to_gif(img_data)
|
||||||
await evt.send(await UniMessage().image(raw=img_data).export())
|
await evt.send(await UniMessage().image(raw=img_data).export())
|
||||||
await evt.send(await UniMessage().text(f"你们太菜了,全部扣100分!明明还可以接「{avaliable_idiom}」的!").export())
|
await evt.send(await UniMessage().text(f"你们太菜了,全部扣100分!明明还可以接「{avaliable_idiom}」的!").export())
|
||||||
idiom = await instance.skip_idiom(-100)
|
idiom = await instance.skip_idiom(-100)
|
||||||
|
|||||||
46
konabot/plugins/k8x12S/__init__.py
Normal file
46
konabot/plugins/k8x12S/__init__.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import re
|
||||||
|
from nonebot import on_message
|
||||||
|
from nonebot.rule import StartswithRule
|
||||||
|
from nonebot_plugin_alconna import At, Text, UniMessage, UniMsg
|
||||||
|
|
||||||
|
from konabot.common.longtask import DepLongTaskTarget
|
||||||
|
from konabot.common.web_render import konaweb
|
||||||
|
from konabot.common.web_render.core import WebRenderer
|
||||||
|
|
||||||
|
|
||||||
|
evt = on_message(rule=StartswithRule(("k8x12s",), True))
|
||||||
|
rule = re.compile(r"^[kK]8[xX]12[sS] ?(.+)$")
|
||||||
|
|
||||||
|
|
||||||
|
@evt.handle()
|
||||||
|
async def _(msg: UniMsg, target: DepLongTaskTarget):
|
||||||
|
if len(msg.include(Text, At)) != len(msg):
|
||||||
|
return
|
||||||
|
|
||||||
|
text = msg.extract_plain_text()
|
||||||
|
result = re.match(rule, text)
|
||||||
|
if result is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
obj: str | None = result.group(1)
|
||||||
|
if obj is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
img = await render_with_web_renderer(obj)
|
||||||
|
await target.send_message(UniMessage.image(raw=img), at=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def render_with_web_renderer(text: str) -> bytes:
|
||||||
|
async def page_function(page):
|
||||||
|
await page.wait_for_selector('textarea[name=content]')
|
||||||
|
await page.locator('textarea[name=content]').fill(text)
|
||||||
|
|
||||||
|
out = await WebRenderer.render_with_persistent_page(
|
||||||
|
"markdown_renderer",
|
||||||
|
konaweb('k8x12S'),
|
||||||
|
target='#main',
|
||||||
|
other_function=page_function,
|
||||||
|
)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
57
konabot/plugins/mc_count_player/__init__.py
Normal file
57
konabot/plugins/mc_count_player/__init__.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import asyncio
|
||||||
|
import mcstatus
|
||||||
|
|
||||||
|
from nonebot import on_command
|
||||||
|
from nonebot.adapters import Event
|
||||||
|
from nonebot_plugin_alconna import UniMessage
|
||||||
|
from konabot.common.nb.is_admin import is_admin
|
||||||
|
from mcstatus.responses import JavaStatusResponse
|
||||||
|
|
||||||
|
|
||||||
|
cmd = on_command("宾几人", aliases=set(("宾人数", "mcbingo")), rule=is_admin)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_status(motd: str) -> str:
|
||||||
|
if "[PRE-GAME]" in motd:
|
||||||
|
return "[✨ 空闲]"
|
||||||
|
if "[IN-GAME]" in motd:
|
||||||
|
return "[🕜 游戏中]"
|
||||||
|
if "[POST-GAME]" in motd:
|
||||||
|
return "[🕜 游戏中]"
|
||||||
|
return "[✨ 开放]"
|
||||||
|
|
||||||
|
|
||||||
|
def dump_server_status(name: str, status: JavaStatusResponse | BaseException) -> str:
|
||||||
|
if isinstance(status, JavaStatusResponse):
|
||||||
|
motd = status.motd.to_plain()
|
||||||
|
# Bingo Status: [PRE-GAME], [IN-GAME], [POST-GAME]
|
||||||
|
st = parse_status(motd)
|
||||||
|
players_sample = status.players.sample or []
|
||||||
|
players_sample_suffix = ""
|
||||||
|
if len(players_sample) > 0:
|
||||||
|
player_list = [s.name for s in players_sample]
|
||||||
|
players_sample_suffix = " (" + ", ".join(player_list) + ")"
|
||||||
|
return f"{name}: {st} {status.players.online} 人在线{players_sample_suffix}"
|
||||||
|
else:
|
||||||
|
return f"{name}: 好像没开"
|
||||||
|
|
||||||
|
|
||||||
|
@cmd.handle()
|
||||||
|
async def _(evt: Event):
|
||||||
|
servers = (
|
||||||
|
(mcstatus.JavaServer("play.simpfun.cn", 11495), "小帕 Bingo"),
|
||||||
|
(mcstatus.JavaServer("bingo.mujica.tech"), "坏枪 Bingo"),
|
||||||
|
(mcstatus.JavaServer("mc.mujica.tech", 11456), "齿轮盛宴"),
|
||||||
|
)
|
||||||
|
|
||||||
|
responses = await asyncio.gather(
|
||||||
|
*map(lambda s: s[0].async_status(), servers),
|
||||||
|
return_exceptions=True,
|
||||||
|
)
|
||||||
|
messages = "\n".join((
|
||||||
|
dump_server_status(n, r)
|
||||||
|
for n, r in zip(map(lambda s: s[1], servers), responses)
|
||||||
|
))
|
||||||
|
|
||||||
|
await UniMessage.text(messages).finish(evt, at_sender=False)
|
||||||
|
|
||||||
@ -1,8 +1,11 @@
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
import random
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from konabot.common.web_render import konaweb
|
from konabot.common.web_render import konaweb
|
||||||
from konabot.common.web_render.core import WebRenderer
|
from konabot.common.web_render.core import WebRenderer
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
from playwright.async_api import Page
|
from playwright.async_api import Page
|
||||||
|
|
||||||
class NoticeUI:
|
class NoticeUI:
|
||||||
@ -18,13 +21,19 @@ class NoticeUI:
|
|||||||
# 直到 setContent 函数加载完成
|
# 直到 setContent 函数加载完成
|
||||||
await page.wait_for_function("typeof setContent === 'function'", timeout=1000)
|
await page.wait_for_function("typeof setContent === 'function'", timeout=1000)
|
||||||
# 设置标题和消息内容
|
# 设置标题和消息内容
|
||||||
await page.evaluate(f'setContent("{title}", "{message}")')
|
await page.evaluate("""([title, message]) => {
|
||||||
|
return setContent(title, message);
|
||||||
|
}""",
|
||||||
|
[title, message])
|
||||||
|
|
||||||
async def mask_function(page: Page):
|
async def mask_function(page: Page):
|
||||||
# 直到 setContent 函数加载完成
|
# 直到 setContent 函数加载完成
|
||||||
await page.wait_for_function("typeof setContent === 'function'", timeout=1000)
|
await page.wait_for_function("typeof setContent === 'function'", timeout=1000)
|
||||||
# 设置标题和消息内容
|
# 设置标题和消息内容
|
||||||
await page.evaluate(f'setContent("{title}", "{message}")')
|
await page.evaluate("""([title, message]) => {
|
||||||
|
return setContent(title, message);
|
||||||
|
}""",
|
||||||
|
[title, message])
|
||||||
# 直到 setMaskMode 函数加载完成
|
# 直到 setMaskMode 函数加载完成
|
||||||
await page.wait_for_function("typeof setMaskMode === 'function'", timeout=1000)
|
await page.wait_for_function("typeof setMaskMode === 'function'", timeout=1000)
|
||||||
await page.evaluate('setMaskMode(true)')
|
await page.evaluate('setMaskMode(true)')
|
||||||
@ -44,33 +53,16 @@ class NoticeUI:
|
|||||||
|
|
||||||
image = Image.open(BytesIO(image_bytes)).convert("RGBA")
|
image = Image.open(BytesIO(image_bytes)).convert("RGBA")
|
||||||
mask = Image.open(BytesIO(mask_bytes)).convert("L")
|
mask = Image.open(BytesIO(mask_bytes)).convert("L")
|
||||||
|
# 遮罩抖动二值化
|
||||||
# 使用mask作为alpha通道
|
mask = mask.convert('1') # 先转换为1位图像
|
||||||
r, g, b, _ = image.split()
|
image.putalpha(mask)
|
||||||
transparent_image = Image.merge("RGBA", (r, g, b, mask))
|
|
||||||
|
|
||||||
# 先创建一个纯白色背景,然后粘贴透明图像
|
|
||||||
background = Image.new("RGBA", transparent_image.size, (255, 255, 255, 255))
|
|
||||||
composite = Image.alpha_composite(background, transparent_image)
|
|
||||||
|
|
||||||
palette_img = composite.convert("RGB").convert(
|
|
||||||
"P",
|
|
||||||
palette=Image.Palette.WEB,
|
|
||||||
colors=256,
|
|
||||||
dither=Image.Dither.NONE
|
|
||||||
)
|
|
||||||
|
|
||||||
# 将alpha值小于128的设为透明
|
|
||||||
alpha_mask = mask.point(lambda x: 0 if x < 128 else 255)
|
|
||||||
|
|
||||||
# 保存为GIF
|
# 保存为GIF
|
||||||
output_buffer = BytesIO()
|
output_buffer = BytesIO()
|
||||||
palette_img.save(
|
image.save(
|
||||||
output_buffer,
|
output_buffer,
|
||||||
format="GIF",
|
format="GIF",
|
||||||
transparency=0, # 将索引0设为透明
|
disposal=2
|
||||||
disposal=2,
|
|
||||||
loop=0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
output_buffer.seek(0)
|
output_buffer.seek(0)
|
||||||
|
|||||||
@ -1,10 +1,58 @@
|
|||||||
from nonebot import on_message
|
from nonebot import on_message
|
||||||
from nonebot_plugin_alconna import UniMessage
|
from nonebot.internal.adapter import Event
|
||||||
|
from nonebot_plugin_alconna import UniMessage, UniMsg, Text
|
||||||
|
|
||||||
from konabot.common.nb.match_keyword import match_keyword
|
from konabot.common.nb.match_keyword import match_keyword
|
||||||
|
|
||||||
evt = on_message(rule=match_keyword("喵"))
|
evt_nya = on_message(rule=match_keyword("喵"))
|
||||||
|
|
||||||
@evt.handle()
|
@evt_nya.handle()
|
||||||
async def _():
|
async def _():
|
||||||
await evt.send(await UniMessage().text("喵").export())
|
await evt_nya.send(await UniMessage().text("喵").export())
|
||||||
|
|
||||||
|
|
||||||
|
NYA_SYMBOL_MAPPING = {
|
||||||
|
"喵": "喵",
|
||||||
|
"!": "!",
|
||||||
|
"?": "!",
|
||||||
|
"!": "!",
|
||||||
|
"?": "!",
|
||||||
|
",": ",",
|
||||||
|
",": ",",
|
||||||
|
".": ".",
|
||||||
|
"。": "。",
|
||||||
|
"…": "…",
|
||||||
|
"~": "~",
|
||||||
|
"~": "~",
|
||||||
|
" ": " ",
|
||||||
|
"\n": "\n",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def has_nya(msg: UniMsg) -> bool:
|
||||||
|
if any((not isinstance(seg, Text) for seg in msg)):
|
||||||
|
return False
|
||||||
|
|
||||||
|
text = msg.extract_plain_text()
|
||||||
|
|
||||||
|
if len(text) <= 1:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if "喵" not in text:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if any(((char not in NYA_SYMBOL_MAPPING) for char in text)):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
evt_nya_v2 = on_message(rule=has_nya)
|
||||||
|
|
||||||
|
@evt_nya_v2.handle()
|
||||||
|
async def _(msg: UniMsg, evt: Event):
|
||||||
|
text = msg.extract_plain_text()
|
||||||
|
await UniMessage.text(''.join(
|
||||||
|
(NYA_SYMBOL_MAPPING.get(c, '') for c in text)
|
||||||
|
)).send(evt)
|
||||||
|
|
||||||
|
|||||||
546
konabot/plugins/oracle_game/__init__.py
Normal file
546
konabot/plugins/oracle_game/__init__.py
Normal file
@ -0,0 +1,546 @@
|
|||||||
|
import asyncio as asynkio
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import secrets
|
||||||
|
import csv
|
||||||
|
import zipfile
|
||||||
|
from PIL import Image
|
||||||
|
from io import BytesIO
|
||||||
|
from enum import Enum
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
from nonebot import on_message
|
||||||
|
import nonebot
|
||||||
|
from nonebot.adapters import Event as BaseEvent
|
||||||
|
from nonebot.adapters.discord.event import MessageEvent as DiscordMessageEvent
|
||||||
|
from nonebot_plugin_alconna import (
|
||||||
|
Alconna,
|
||||||
|
Args,
|
||||||
|
UniMessage,
|
||||||
|
UniMsg,
|
||||||
|
on_alconna,
|
||||||
|
)
|
||||||
|
|
||||||
|
from konabot.common.database import DatabaseManager
|
||||||
|
from konabot.common.longtask import DepLongTaskTarget
|
||||||
|
from konabot.common.path import ASSETS_PATH
|
||||||
|
|
||||||
|
from konabot.plugins.hanzi import get_char
|
||||||
|
|
||||||
|
ROOT_PATH = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
DATA_DIR = Path(__file__).parent.parent.parent.parent / "data"
|
||||||
|
|
||||||
|
DATA_FILE_PATH = (
|
||||||
|
DATA_DIR / "oracle_banned.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建全局数据库管理器实例
|
||||||
|
db_manager = DatabaseManager()
|
||||||
|
|
||||||
|
def load_banned_ids() -> list[str]:
|
||||||
|
if not DATA_FILE_PATH.exists():
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
return json.loads(DATA_FILE_PATH.read_text("utf-8"))
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"在解析甲骨文封禁文件时遇到问题:{e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def is_oracle_game_banned(group_id: str) -> bool:
|
||||||
|
banned_ids = load_banned_ids()
|
||||||
|
return group_id in banned_ids
|
||||||
|
|
||||||
|
|
||||||
|
def add_banned_id(group_id: str):
|
||||||
|
banned_ids = load_banned_ids()
|
||||||
|
if group_id not in banned_ids:
|
||||||
|
banned_ids.append(group_id)
|
||||||
|
DATA_FILE_PATH.write_text(json.dumps(banned_ids, ensure_ascii=False, indent=4), "utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def remove_banned_id(group_id: str):
|
||||||
|
banned_ids = load_banned_ids()
|
||||||
|
if group_id in banned_ids:
|
||||||
|
banned_ids.remove(group_id)
|
||||||
|
DATA_FILE_PATH.write_text(json.dumps(banned_ids, ensure_ascii=False, indent=4), "utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
driver = nonebot.get_driver()
|
||||||
|
|
||||||
|
|
||||||
|
@driver.on_startup
|
||||||
|
async def register_startup_hook():
|
||||||
|
"""注册启动时需要执行的函数"""
|
||||||
|
await oracleGame.init_lexicon()
|
||||||
|
|
||||||
|
@driver.on_shutdown
|
||||||
|
async def register_shutdown_hook():
|
||||||
|
"""注册关闭时需要执行的函数"""
|
||||||
|
# 关闭所有数据库连接
|
||||||
|
await db_manager.close_all_connections()
|
||||||
|
|
||||||
|
|
||||||
|
class TryStartState(Enum):
|
||||||
|
STARTED = 0
|
||||||
|
ALREADY_PLAYING = 1
|
||||||
|
NO_REMAINING_TIMES = 2
|
||||||
|
|
||||||
|
|
||||||
|
class TryStopState(Enum):
|
||||||
|
STOPPED = 0
|
||||||
|
NOT_PLAYING = 1
|
||||||
|
|
||||||
|
class TryVerifyState(Enum):
|
||||||
|
VERIFIED = 0
|
||||||
|
NOT_ORACLE = 1
|
||||||
|
HINT_ONE = 2
|
||||||
|
HINT_TWO = 3
|
||||||
|
GAME_END = 4
|
||||||
|
|
||||||
|
class oracleGame:
|
||||||
|
ALL_ORACLES = {}
|
||||||
|
INSTANCE_LIST: dict[str, "oracleGame"] = {} # 群号对应的游戏实例
|
||||||
|
__inited = False
|
||||||
|
|
||||||
|
def __init__(self, group_id: str):
|
||||||
|
# 初始化一局游戏
|
||||||
|
self.group_id = ""
|
||||||
|
self.now_playing = False
|
||||||
|
self.score_board = {}
|
||||||
|
self.remain_playing_times = 3
|
||||||
|
self.last_play_date = ""
|
||||||
|
self.all_buff_score = 0
|
||||||
|
self.lock = asynkio.Lock()
|
||||||
|
self.remain_rounds = 0 # 剩余回合数
|
||||||
|
self.current_oracle_id = ""
|
||||||
|
self.wrong_attempts = 0
|
||||||
|
oracleGame.INSTANCE_LIST[group_id] = self
|
||||||
|
|
||||||
|
def be_able_to_play(self) -> bool:
|
||||||
|
if self.last_play_date != datetime.date.today():
|
||||||
|
self.last_play_date = datetime.date.today()
|
||||||
|
self.remain_playing_times = 3
|
||||||
|
if self.remain_playing_times > 0:
|
||||||
|
self.remain_playing_times -= 1
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_oracle_image(self) -> bytes:
|
||||||
|
IMAGE_PATH = ASSETS_PATH / "oracle" / "image"
|
||||||
|
with open(IMAGE_PATH / self.ALL_ORACLES[self.current_oracle_id]["image"], "rb") as f:
|
||||||
|
img_data = f.read()
|
||||||
|
return img_data
|
||||||
|
|
||||||
|
def get_oracle_name(self) -> str:
|
||||||
|
return self.ALL_ORACLES.get(self.current_oracle_id, {}).get("oracle", "?")[0]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def random_oracle() -> str:
|
||||||
|
return secrets.choice(list(oracleGame.ALL_ORACLES.keys()))
|
||||||
|
|
||||||
|
async def choose_start_oracle(self) -> str:
|
||||||
|
"""
|
||||||
|
随机选择一个甲骨文作为起始甲骨文
|
||||||
|
"""
|
||||||
|
self.current_oracle_id = await oracleGame.random_oracle()
|
||||||
|
return self.current_oracle_id
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def try_start_game(cls, group_id: str, force: bool = False) -> TryStartState:
|
||||||
|
await cls.init_lexicon()
|
||||||
|
if not cls.INSTANCE_LIST.get(group_id):
|
||||||
|
cls(group_id)
|
||||||
|
instance = cls.INSTANCE_LIST[group_id]
|
||||||
|
if instance.now_playing:
|
||||||
|
return TryStartState.ALREADY_PLAYING
|
||||||
|
if not instance.be_able_to_play() and not force:
|
||||||
|
return TryStartState.NO_REMAINING_TIMES
|
||||||
|
instance.now_playing = True
|
||||||
|
return TryStartState.STARTED
|
||||||
|
|
||||||
|
async def start_game(self, rounds: int = 100):
|
||||||
|
self.now_playing = True
|
||||||
|
self.remain_rounds = rounds
|
||||||
|
await self.choose_start_oracle()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def try_stop_game(cls, group_id: str) -> TryStopState:
|
||||||
|
if not cls.INSTANCE_LIST.get(group_id):
|
||||||
|
return TryStopState.NOT_PLAYING
|
||||||
|
instance = cls.INSTANCE_LIST[group_id]
|
||||||
|
if not instance.now_playing:
|
||||||
|
return TryStopState.NOT_PLAYING
|
||||||
|
instance.now_playing = False
|
||||||
|
return TryStopState.STOPPED
|
||||||
|
|
||||||
|
def clear_score_board(self):
|
||||||
|
self.wrong_attempts = 0
|
||||||
|
self.score_board = {}
|
||||||
|
self.all_buff_score = 0
|
||||||
|
|
||||||
|
def get_score_board(self) -> dict:
|
||||||
|
return self.score_board
|
||||||
|
|
||||||
|
def get_all_buff_score(self) -> int:
|
||||||
|
return self.all_buff_score
|
||||||
|
|
||||||
|
async def skip_oracle(self, buff_score: int = -100) -> str:
|
||||||
|
"""
|
||||||
|
跳过当前甲骨文,选择下一个甲骨文
|
||||||
|
"""
|
||||||
|
async with self.lock:
|
||||||
|
await self._skip_oracle_async()
|
||||||
|
self.add_buff_score(buff_score)
|
||||||
|
return self.current_oracle_id
|
||||||
|
|
||||||
|
async def _skip_oracle_async(self) -> str:
|
||||||
|
self.wrong_attempts = 0
|
||||||
|
self.current_oracle_id = await oracleGame.random_oracle()
|
||||||
|
return self.current_oracle_id
|
||||||
|
|
||||||
|
async def try_verify_oracle(self, oracle: str, user_id: str) -> list[TryVerifyState]:
|
||||||
|
"""
|
||||||
|
用户发送甲骨文
|
||||||
|
"""
|
||||||
|
async with self.lock:
|
||||||
|
state = await self._verify_oracle(oracle, user_id)
|
||||||
|
return state
|
||||||
|
|
||||||
|
async def _verify_oracle(self, oracle: str, user_id: str) -> list[TryVerifyState]:
|
||||||
|
state = []
|
||||||
|
if oracle.strip() not in self.ALL_ORACLES[self.current_oracle_id].get("oracle", ""):
|
||||||
|
state.append(TryVerifyState.NOT_ORACLE)
|
||||||
|
self.wrong_attempts += 1
|
||||||
|
if self.wrong_attempts == 5:
|
||||||
|
state.append(TryVerifyState.HINT_ONE)
|
||||||
|
elif self.wrong_attempts == 10:
|
||||||
|
state.append(TryVerifyState.HINT_TWO)
|
||||||
|
return state
|
||||||
|
if oracle.strip() == "":
|
||||||
|
return [TryVerifyState.NOT_ORACLE]
|
||||||
|
# 甲骨文合法,更新状态
|
||||||
|
self.wrong_attempts = 0
|
||||||
|
state.append(TryVerifyState.VERIFIED)
|
||||||
|
self.add_score(user_id, 1) # 加 1 分
|
||||||
|
self.remain_rounds -= 1
|
||||||
|
if self.remain_rounds <= 0:
|
||||||
|
self.now_playing = False
|
||||||
|
state.append(TryVerifyState.GAME_END)
|
||||||
|
else:
|
||||||
|
await self._skip_oracle_async()
|
||||||
|
return state
|
||||||
|
|
||||||
|
def get_user_score(self, user_id: str) -> float:
|
||||||
|
if user_id not in self.score_board:
|
||||||
|
return 0
|
||||||
|
# 避免浮点数精度问题导致过长
|
||||||
|
handled_score = round(self.score_board[user_id]["score"] + self.all_buff_score, 1)
|
||||||
|
return handled_score
|
||||||
|
|
||||||
|
def add_score(self, user_id: str, score: int):
|
||||||
|
if user_id not in self.score_board:
|
||||||
|
self.score_board[user_id] = {"name": user_id, "score": 0}
|
||||||
|
self.score_board[user_id]["score"] += score
|
||||||
|
|
||||||
|
def add_buff_score(self, score: int):
|
||||||
|
self.all_buff_score += score
|
||||||
|
|
||||||
|
def get_playing_state(self) -> bool:
|
||||||
|
return self.now_playing
|
||||||
|
|
||||||
|
def get_pinyin_hint(self) -> str:
|
||||||
|
return self.ALL_ORACLES[self.current_oracle_id].get("pinyin", "无")
|
||||||
|
|
||||||
|
def get_meaning_hint(self) -> str:
|
||||||
|
return self.ALL_ORACLES[self.current_oracle_id].get("meaning", "无")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def init_lexicon(cls):
|
||||||
|
if cls.__inited:
|
||||||
|
return
|
||||||
|
cls.__inited = True
|
||||||
|
|
||||||
|
# 加载甲骨文
|
||||||
|
ORACLE_DATA_PATH = ASSETS_PATH / "oracle"
|
||||||
|
|
||||||
|
with open(ORACLE_DATA_PATH / "zi_dict.csv", "r", encoding="utf-8-sig") as f:
|
||||||
|
reader = csv.DictReader(f)
|
||||||
|
# 以“子字头”为key,释文为value,构建字典
|
||||||
|
for row in reader:
|
||||||
|
char = row["子字头"].strip()
|
||||||
|
oracle = row["释文"].strip()
|
||||||
|
img_path = row.get("路径", "").strip()
|
||||||
|
cls.ALL_ORACLES[char] = {
|
||||||
|
"oracle": oracle,
|
||||||
|
"image": img_path,
|
||||||
|
"pinyin": row.get("拼音", "").strip(),
|
||||||
|
"meaning": row.get("含义", "").strip(),
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"加载甲骨文字典,共计 {len(cls.ALL_ORACLES)} 条记录")
|
||||||
|
|
||||||
|
# 解包图片资源
|
||||||
|
IMAGE_PATH = ASSETS_PATH / "oracle" / "image"
|
||||||
|
|
||||||
|
if not IMAGE_PATH.exists():
|
||||||
|
IMAGE_PATH.mkdir(parents=True, exist_ok=True)
|
||||||
|
# 将 image.zip 解压到 IMAGE_PATH
|
||||||
|
if (ASSETS_PATH / "oracle" / "image.zip").exists():
|
||||||
|
with zipfile.ZipFile(ASSETS_PATH / "oracle" / "image.zip", "r") as zip_ref:
|
||||||
|
zip_ref.extractall(IMAGE_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
evt = on_alconna(
|
||||||
|
Alconna(
|
||||||
|
"我要玩甲骨文",
|
||||||
|
Args["rounds?", int],
|
||||||
|
),
|
||||||
|
use_cmd_start=True,
|
||||||
|
use_cmd_sep=False,
|
||||||
|
skip_for_unmatch=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@evt.handle()
|
||||||
|
async def play_game(
|
||||||
|
event: BaseEvent,
|
||||||
|
target: DepLongTaskTarget,
|
||||||
|
force=False,
|
||||||
|
rounds: Optional[int] = 100,
|
||||||
|
):
|
||||||
|
# group_id = str(event.get_session_id())
|
||||||
|
group_id = target.channel_id
|
||||||
|
if is_oracle_game_banned(group_id):
|
||||||
|
await evt.send(
|
||||||
|
await UniMessage().text("本群已被禁止使用甲骨文功能!").export()
|
||||||
|
)
|
||||||
|
return
|
||||||
|
rounds = rounds or 0
|
||||||
|
if rounds <= 0:
|
||||||
|
await evt.send(await UniMessage().text("干什么!你想玩负数局吗?").export())
|
||||||
|
return
|
||||||
|
state = await oracleGame.try_start_game(group_id, force)
|
||||||
|
if state == TryStartState.ALREADY_PLAYING:
|
||||||
|
await evt.send(
|
||||||
|
await UniMessage()
|
||||||
|
.text("当前已有甲骨文游戏在进行中,请稍后再试!")
|
||||||
|
.export()
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if state == TryStartState.NO_REMAINING_TIMES:
|
||||||
|
await evt.send(await UniMessage().text("玩玩玩,就知道玩,快去睡觉!").export())
|
||||||
|
return
|
||||||
|
await evt.send(
|
||||||
|
await UniMessage()
|
||||||
|
.text(
|
||||||
|
"你小子,还真有意思!\n好,甲骨文游戏开始!我发一个甲骨文,尼赖硕!"
|
||||||
|
)
|
||||||
|
.export()
|
||||||
|
)
|
||||||
|
instance = oracleGame.INSTANCE_LIST[group_id]
|
||||||
|
await instance.start_game(rounds)
|
||||||
|
# 发布甲骨文
|
||||||
|
await evt.send(
|
||||||
|
await UniMessage()
|
||||||
|
.image(raw=instance.get_oracle_image())
|
||||||
|
.export()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
evt = on_alconna(
|
||||||
|
Alconna(
|
||||||
|
"老子就是要玩甲骨文!!!",
|
||||||
|
Args["rounds?", int],
|
||||||
|
),
|
||||||
|
use_cmd_start=True,
|
||||||
|
use_cmd_sep=False,
|
||||||
|
skip_for_unmatch=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@evt.handle()
|
||||||
|
async def force_play_game(
|
||||||
|
event: BaseEvent, target: DepLongTaskTarget, rounds: Optional[int] = 100
|
||||||
|
):
|
||||||
|
await play_game(event, target, force=True, rounds=rounds)
|
||||||
|
|
||||||
|
|
||||||
|
async def end_game(event: BaseEvent, group_id: str):
|
||||||
|
instance = oracleGame.INSTANCE_LIST[group_id]
|
||||||
|
result_text = UniMessage().text("游戏结束!\n最终得分榜:\n")
|
||||||
|
score_board = instance.get_score_board()
|
||||||
|
if len(score_board) == 0:
|
||||||
|
result_text += "无人得分!\n"
|
||||||
|
else:
|
||||||
|
# 按分数排序,名字用 at 的方式
|
||||||
|
sorted_score = sorted(
|
||||||
|
score_board.items(), key=lambda x: x[1]["score"], reverse=True
|
||||||
|
)
|
||||||
|
for i, (user_id, info) in enumerate(sorted_score):
|
||||||
|
result_text += (
|
||||||
|
f"{i + 1}. "
|
||||||
|
+ UniMessage().at(user_id)
|
||||||
|
+ f": {round(info['score'] + instance.get_all_buff_score(), 1)} 分\n"
|
||||||
|
)
|
||||||
|
await evt.send(await result_text.export())
|
||||||
|
# instance.clear_score_board()
|
||||||
|
# 将实例删除
|
||||||
|
del oracleGame.INSTANCE_LIST[group_id]
|
||||||
|
|
||||||
|
|
||||||
|
evt = on_alconna(
|
||||||
|
Alconna("不玩了"), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@evt.handle()
|
||||||
|
async def _(event: BaseEvent, target: DepLongTaskTarget):
|
||||||
|
# group_id = str(event.get_session_id())
|
||||||
|
group_id = target.channel_id
|
||||||
|
state = oracleGame.try_stop_game(group_id)
|
||||||
|
if state == TryStopState.STOPPED:
|
||||||
|
# 发送好吧狗图片
|
||||||
|
# 打开好吧狗本地文件
|
||||||
|
with open(ASSETS_PATH / "img" / "dog" / "haoba_dog.jpg", "rb") as f:
|
||||||
|
img_data = f.read()
|
||||||
|
# 把好吧狗变成 GIF 格式以缩小尺寸
|
||||||
|
img_data = await convert_image_to_gif(img_data)
|
||||||
|
await evt.send(await UniMessage().image(raw=img_data).export())
|
||||||
|
await end_game(event, group_id)
|
||||||
|
else:
|
||||||
|
# await evt.send(
|
||||||
|
# await UniMessage().text("当前没有甲骨文游戏在进行中!").export()
|
||||||
|
# )
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
async def convert_image_to_gif(image_data: bytes) -> bytes:
|
||||||
|
with Image.open(BytesIO(image_data)) as img:
|
||||||
|
with BytesIO() as output:
|
||||||
|
img.save(output, format="GIF")
|
||||||
|
return output.getvalue()
|
||||||
|
# 跳过
|
||||||
|
evt = on_alconna(
|
||||||
|
Alconna("跳过甲骨文"), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@evt.handle()
|
||||||
|
async def _(target: DepLongTaskTarget):
|
||||||
|
# group_id = str(event.get_session_id())
|
||||||
|
group_id = target.channel_id
|
||||||
|
instance = oracleGame.INSTANCE_LIST.get(group_id)
|
||||||
|
if not instance or not instance.get_playing_state():
|
||||||
|
return
|
||||||
|
# 发送哈哈狗图片
|
||||||
|
with open(ASSETS_PATH / "img" / "dog" / "haha_dog.jpg", "rb") as f:
|
||||||
|
img_data = f.read()
|
||||||
|
# 把哈哈狗变成 GIF 格式以缩小尺寸
|
||||||
|
img_data = await convert_image_to_gif(img_data)
|
||||||
|
oracle = instance.get_oracle_name()
|
||||||
|
await evt.send(await UniMessage().image(raw=img_data).export())
|
||||||
|
await evt.send(await UniMessage().text(f"你们太菜了,全部扣100分!这个甲骨文是「{oracle}」!").export())
|
||||||
|
oracle = await instance.skip_oracle(-100)
|
||||||
|
await evt.send(
|
||||||
|
await UniMessage()
|
||||||
|
.image(raw=instance.get_oracle_image())
|
||||||
|
.export()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_info(event: BaseEvent):
|
||||||
|
if isinstance(event, DiscordMessageEvent):
|
||||||
|
user_id = str(event.author.id)
|
||||||
|
user_name = str(event.author.name)
|
||||||
|
else:
|
||||||
|
user_id = str(event.get_user_id())
|
||||||
|
user_name = str(event.get_user_id())
|
||||||
|
return user_id, user_name
|
||||||
|
|
||||||
|
|
||||||
|
# 直接读取消息
|
||||||
|
evt = on_message()
|
||||||
|
|
||||||
|
|
||||||
|
@evt.handle()
|
||||||
|
async def _(event: BaseEvent, msg: UniMsg, target: DepLongTaskTarget):
|
||||||
|
# group_id = str(event.get_session_id())
|
||||||
|
group_id = target.channel_id
|
||||||
|
instance = oracleGame.INSTANCE_LIST.get(group_id)
|
||||||
|
if not instance or not instance.get_playing_state():
|
||||||
|
return
|
||||||
|
user_oracle = msg.extract_plain_text().strip()
|
||||||
|
# 甲骨文应该是单个汉字
|
||||||
|
if len(user_oracle) != 1:
|
||||||
|
return
|
||||||
|
user_id, user_name = get_user_info(event)
|
||||||
|
state = await instance.try_verify_oracle(user_oracle, user_id)
|
||||||
|
if TryVerifyState.HINT_ONE in state:
|
||||||
|
hint_pinyin = instance.get_pinyin_hint()
|
||||||
|
await evt.send(
|
||||||
|
await UniMessage()
|
||||||
|
.text(f"提示:这个甲骨文的拼音是「{hint_pinyin}」")
|
||||||
|
.export()
|
||||||
|
)
|
||||||
|
if TryVerifyState.HINT_TWO in state:
|
||||||
|
hint_meaning = instance.get_meaning_hint()
|
||||||
|
await evt.send(
|
||||||
|
await UniMessage()
|
||||||
|
.text(f"提示:这个甲骨文的含义是「{hint_meaning}」")
|
||||||
|
.export()
|
||||||
|
)
|
||||||
|
if TryVerifyState.NOT_ORACLE in state:
|
||||||
|
return
|
||||||
|
if TryVerifyState.VERIFIED in state:
|
||||||
|
await evt.send(
|
||||||
|
await UniMessage()
|
||||||
|
.at(user_id)
|
||||||
|
.text(" 答对了!获得 1 分!")
|
||||||
|
.export()
|
||||||
|
)
|
||||||
|
if TryVerifyState.GAME_END in state:
|
||||||
|
await evt.send(await UniMessage().text("全部回合结束!").export())
|
||||||
|
await end_game(event, group_id)
|
||||||
|
return
|
||||||
|
await evt.send(
|
||||||
|
await UniMessage()
|
||||||
|
.image(raw=instance.get_oracle_image())
|
||||||
|
.export()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
evt = on_alconna(
|
||||||
|
Alconna("禁止甲骨文"),
|
||||||
|
use_cmd_start=True,
|
||||||
|
use_cmd_sep=False,
|
||||||
|
skip_for_unmatch=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@evt.handle()
|
||||||
|
async def _(event: BaseEvent, target: DepLongTaskTarget):
|
||||||
|
# group_id = str(event.get_session_id())
|
||||||
|
group_id = target.channel_id
|
||||||
|
add_banned_id(group_id)
|
||||||
|
await evt.send(await UniMessage().text("本群已被禁止使用甲骨文功能!").export())
|
||||||
|
|
||||||
|
|
||||||
|
evt = on_alconna(
|
||||||
|
Alconna("开启甲骨文"),
|
||||||
|
use_cmd_start=True,
|
||||||
|
use_cmd_sep=False,
|
||||||
|
skip_for_unmatch=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@evt.handle()
|
||||||
|
async def _(event: BaseEvent, target: DepLongTaskTarget):
|
||||||
|
# group_id = str(event.get_session_id())
|
||||||
|
group_id = target.channel_id
|
||||||
|
remove_banned_id(group_id)
|
||||||
|
await evt.send(await UniMessage().text("本群已开启甲骨文功能!").export())
|
||||||
59
konabot/plugins/oracle_game/find_path.py
Normal file
59
konabot/plugins/oracle_game/find_path.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import csv
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
# 获取当前文件所在目录
|
||||||
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
# 向上两级到 konabot 目录
|
||||||
|
konabot_dir = os.path.abspath(os.path.join(current_dir, '../../../'))
|
||||||
|
|
||||||
|
if konabot_dir not in sys.path:
|
||||||
|
sys.path.insert(0, konabot_dir)
|
||||||
|
|
||||||
|
from konabot.common.path import ASSETS_PATH
|
||||||
|
|
||||||
|
|
||||||
|
ORACLE_PATH = ASSETS_PATH / "oracle"
|
||||||
|
|
||||||
|
final_zi_dict = {}
|
||||||
|
with open(ORACLE_PATH / "zi_dict.csv", "r", encoding="utf-8-sig") as f:
|
||||||
|
reader = csv.DictReader(f)
|
||||||
|
for row in reader:
|
||||||
|
logger.info(f"Progress: {reader.line_num}")
|
||||||
|
# 找到子字头字段,并找到对应的图像路径
|
||||||
|
char = row["子字头"].strip()
|
||||||
|
# 寻找路径
|
||||||
|
image_path = ORACLE_PATH / "image"
|
||||||
|
# 遍历所有子目录,寻找对应的图片文件
|
||||||
|
found_image = None
|
||||||
|
for subdir in image_path.iterdir():
|
||||||
|
if subdir.is_dir():
|
||||||
|
candidate = subdir / char
|
||||||
|
if candidate.exists():
|
||||||
|
# 寻找该目录下有没有以 char 命名的图片文件,没有就选第一个图片文件
|
||||||
|
if (candidate / f"{char}.png").exists():
|
||||||
|
found_image = candidate / f"{char}.png"
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
for file in candidate.iterdir():
|
||||||
|
if file.suffix.lower() in [".png", ".jpg", ".jpeg", ".gif", ".bmp"]:
|
||||||
|
found_image = file
|
||||||
|
break
|
||||||
|
if found_image is not None:
|
||||||
|
# 提取相对路径
|
||||||
|
found_image = found_image.relative_to(ORACLE_PATH / "image")
|
||||||
|
# 反斜杠改正为斜杠
|
||||||
|
found_image = found_image.as_posix()
|
||||||
|
# 更新行数据
|
||||||
|
row.update({"路径": str(found_image)})
|
||||||
|
final_zi_dict[char] = row
|
||||||
|
|
||||||
|
# 将最终的字典写入新的 CSV 文件
|
||||||
|
with open(ORACLE_PATH / "zi_dict_with_images.csv", "w", encoding="utf-8-sig", newline="") as f:
|
||||||
|
fieldnames = list(final_zi_dict[next(iter(final_zi_dict))].keys())
|
||||||
|
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
||||||
|
writer.writeheader()
|
||||||
|
for char, data in final_zi_dict.items():
|
||||||
|
writer.writerow(data)
|
||||||
0
konabot/plugins/oracle_game/hanzi_info.py
Normal file
0
konabot/plugins/oracle_game/hanzi_info.py
Normal file
@ -1,4 +1,3 @@
|
|||||||
import re
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import asyncio as asynkio
|
import asyncio as asynkio
|
||||||
from math import ceil
|
from math import ceil
|
||||||
@ -6,6 +5,7 @@ from pathlib import Path
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import nanoid
|
import nanoid
|
||||||
|
from nonebot.rule import KeywordsRule, Rule
|
||||||
from konabot.plugins.notice_ui.notice import NoticeUI
|
from konabot.plugins.notice_ui.notice import NoticeUI
|
||||||
import nonebot
|
import nonebot
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@ -15,10 +15,9 @@ from nonebot_plugin_alconna import Alconna, Args, Subcommand, UniMessage, UniMsg
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from konabot.common.longtask import DepLongTaskTarget, LongTask, create_longtask, handle_long_task, longtask_data
|
from konabot.common.longtask import DepLongTaskTarget, LongTask, create_longtask, handle_long_task, longtask_data
|
||||||
from konabot.common.nb.match_keyword import match_keyword
|
|
||||||
from konabot.plugins.simple_notify.ask_llm import ask_ai
|
from konabot.plugins.simple_notify.ask_llm import ask_ai
|
||||||
|
|
||||||
evt = on_message(rule=match_keyword(re.compile("^.+提醒我.+$")))
|
evt = on_message(rule=Rule(KeywordsRule("提醒我")))
|
||||||
|
|
||||||
(Path(__file__).parent.parent.parent.parent / "data").mkdir(exist_ok=True)
|
(Path(__file__).parent.parent.parent.parent / "data").mkdir(exist_ok=True)
|
||||||
DATA_FILE_PATH = Path(__file__).parent.parent.parent.parent / "data" / "notify.json"
|
DATA_FILE_PATH = Path(__file__).parent.parent.parent.parent / "data" / "notify.json"
|
||||||
@ -96,7 +95,7 @@ async def _(msg: UniMsg, mEvt: Event, target: DepLongTaskTarget):
|
|||||||
await target.send_message(
|
await target.send_message(
|
||||||
UniMessage().text(f"了解啦!将会在 {target_time.strftime(FMT_STRING)} 提醒你哦~")
|
UniMessage().text(f"了解啦!将会在 {target_time.strftime(FMT_STRING)} 提醒你哦~")
|
||||||
)
|
)
|
||||||
logger.info(f"创建了一条于 {target_time} 的代办提醒")
|
logger.info(f"创建了一条于 {target_time} 的待办提醒")
|
||||||
|
|
||||||
|
|
||||||
driver = nonebot.get_driver()
|
driver = nonebot.get_driver()
|
||||||
@ -106,9 +105,9 @@ driver = nonebot.get_driver()
|
|||||||
async def _(task: LongTask):
|
async def _(task: LongTask):
|
||||||
message = task.data["message"]
|
message = task.data["message"]
|
||||||
await task.target.send_message(
|
await task.target.send_message(
|
||||||
UniMessage().text(f"代办提醒:{message}")
|
UniMessage().text(f"待办提醒:{message}")
|
||||||
)
|
)
|
||||||
notice_bytes = await NoticeUI.render_notice("代办提醒", message)
|
notice_bytes = await NoticeUI.render_notice("待办提醒", message)
|
||||||
await task.target.send_message(
|
await task.target.send_message(
|
||||||
UniMessage().image(raw=notice_bytes),
|
UniMessage().image(raw=notice_bytes),
|
||||||
at=False
|
at=False
|
||||||
@ -124,7 +123,7 @@ USER_CHECKOUT_TASK_CACHE: dict[str, dict[str, str]] = {}
|
|||||||
|
|
||||||
|
|
||||||
cmd_check_notify_list = on_alconna(Alconna(
|
cmd_check_notify_list = on_alconna(Alconna(
|
||||||
"re:(?:我有哪些|查询)(?:提醒|代办)",
|
"re:(?:我有哪些|查询)(?:提醒|待办)",
|
||||||
Args["page", int, 1]
|
Args["page", int, 1]
|
||||||
))
|
))
|
||||||
|
|
||||||
@ -142,7 +141,7 @@ async def _(page: int, target: DepLongTaskTarget):
|
|||||||
await target.send_message(UniMessage().text(f"最多也就 {pages} 页啦!"))
|
await target.send_message(UniMessage().text(f"最多也就 {pages} 页啦!"))
|
||||||
tasks = tasks[(page - 1) * PAGE_SIZE: page * PAGE_SIZE]
|
tasks = tasks[(page - 1) * PAGE_SIZE: page * PAGE_SIZE]
|
||||||
|
|
||||||
message = "你可以输入「删除提醒 序号」来删除一个提醒\n====== 代办清单 ======\n\n"
|
message = "你可以输入「删除提醒 序号」来删除一个提醒\n====== 待办清单 ======\n\n"
|
||||||
|
|
||||||
to_cache = {}
|
to_cache = {}
|
||||||
if len(tasks) == 0:
|
if len(tasks) == 0:
|
||||||
@ -159,7 +158,7 @@ async def _(page: int, target: DepLongTaskTarget):
|
|||||||
|
|
||||||
|
|
||||||
cmd_remove_task = on_alconna(Alconna(
|
cmd_remove_task = on_alconna(Alconna(
|
||||||
"re:删除(?:提醒|代办)",
|
"re:删除(?:提醒|待办)",
|
||||||
Args["checker", str],
|
Args["checker", str],
|
||||||
))
|
))
|
||||||
|
|
||||||
@ -236,7 +235,7 @@ async def _(target: DepLongTaskTarget, notify_id: str = ""):
|
|||||||
))
|
))
|
||||||
|
|
||||||
await send_notify_to_ntfy_instance(
|
await send_notify_to_ntfy_instance(
|
||||||
"如果你看到这条消息,说明你已经成功订阅主题!此方 BOT 将会在这里提醒你你的代办!",
|
"如果你看到这条消息,说明你已经成功订阅主题!此方 BOT 将会在这里提醒你你的待办!",
|
||||||
channel_name,
|
channel_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
101
konabot/plugins/solar_terms/__init__.py
Normal file
101
konabot/plugins/solar_terms/__init__.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
from borax.calendars import LunarDate
|
||||||
|
from nonebot import on_command
|
||||||
|
from nonebot.internal.adapter.event import Event
|
||||||
|
from nonebot_plugin_alconna import UniMessage
|
||||||
|
from nonebot_plugin_apscheduler import scheduler
|
||||||
|
|
||||||
|
from konabot.plugins.poster.poster_info import PosterInfo, register_poster_info
|
||||||
|
from konabot.plugins.poster.service import broadcast
|
||||||
|
|
||||||
|
register_poster_info(
|
||||||
|
"二十四节气",
|
||||||
|
PosterInfo(
|
||||||
|
{"节气", "24节气"},
|
||||||
|
"当有新的节气时,报告节气信息",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 二十四节气的内置口号
|
||||||
|
# Generated by claude-opus-4.6
|
||||||
|
SOLAR_TERM_SLOGANS: dict[str, str] = {
|
||||||
|
"立春": "春回大地,万物复苏!",
|
||||||
|
"雨水": "春雨绵绵,润物无声!",
|
||||||
|
"惊蛰": "春雷惊蛰,万物生长!",
|
||||||
|
"春分": "昼夜平分,春意盎然!",
|
||||||
|
"清明": "清明时节,踏青赏春!",
|
||||||
|
"谷雨": "谷雨时节,播种希望!",
|
||||||
|
"立夏": "立夏之日,夏意渐浓!",
|
||||||
|
"小满": "小满时节,麦穗渐满!",
|
||||||
|
"芒种": "芒种农忙,收获在望!",
|
||||||
|
"夏至": "夏至日长,骄阳似火!",
|
||||||
|
"小暑": "小暑炎炎,清凉为伴!",
|
||||||
|
"大暑": "大暑酷热,防暑降温!",
|
||||||
|
"立秋": "立秋时节,暑去凉来!",
|
||||||
|
"处暑": "处暑时节,秋高气爽!",
|
||||||
|
"白露": "白露降临,秋意渐浓!",
|
||||||
|
"秋分": "秋分时节,硕果累累!",
|
||||||
|
"寒露": "寒露凝结,秋意正浓!",
|
||||||
|
"霜降": "霜降时节,秋收冬藏!",
|
||||||
|
"立冬": "立冬之日,冬意渐起!",
|
||||||
|
"小雪": "小雪飘飘,寒意渐浓!",
|
||||||
|
"大雪": "大雪纷飞,银装素裹!",
|
||||||
|
"冬至": "冬至日短,数九寒天!",
|
||||||
|
"小寒": "小寒时节,天寒地冻!",
|
||||||
|
"大寒": "大寒岁末,辞旧迎新!",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@scheduler.scheduled_job("cron", hour="8")
|
||||||
|
async def _():
|
||||||
|
today = LunarDate.today()
|
||||||
|
term: str | None = today.term
|
||||||
|
|
||||||
|
if term is not None:
|
||||||
|
slogan = SOLAR_TERM_SLOGANS.get(term, "")
|
||||||
|
await broadcast(
|
||||||
|
"二十四节气", UniMessage.text(f"【今日节气】今天是 {term} 哦!{slogan}")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
cmd_next_term = on_command("下一个节气")
|
||||||
|
|
||||||
|
|
||||||
|
@cmd_next_term.handle()
|
||||||
|
async def _(event: Event):
|
||||||
|
date = LunarDate.today()
|
||||||
|
day_counter = 0
|
||||||
|
|
||||||
|
while date.term is None:
|
||||||
|
date = date.after(day_delta=1)
|
||||||
|
day_counter += 1
|
||||||
|
if day_counter > 365:
|
||||||
|
await UniMessage.text("哇呀...查询出错了!").send(event)
|
||||||
|
return
|
||||||
|
|
||||||
|
d_cn_format = date.strftime("%M月%D") # 相当于正月初一这样的格式
|
||||||
|
date_solar = date.to_solar_date()
|
||||||
|
d_glob_format = f"{date_solar.month} 月 {date_solar.day} 日"
|
||||||
|
msg = UniMessage.text(
|
||||||
|
f"下一个节气是{date.term},在 {day_counter} 天后的 {d_glob_format}(农历{d_cn_format})"
|
||||||
|
)
|
||||||
|
await msg.send(event)
|
||||||
|
|
||||||
|
|
||||||
|
cmd_current_term = on_command("当前节气", aliases={"获取节气", "节气"})
|
||||||
|
|
||||||
|
|
||||||
|
@cmd_current_term.handle()
|
||||||
|
async def _(event: Event):
|
||||||
|
date = LunarDate.today()
|
||||||
|
day_counter = 0
|
||||||
|
|
||||||
|
while date.term is None:
|
||||||
|
date = date.before(day_delta=1)
|
||||||
|
day_counter += 1
|
||||||
|
if day_counter > 365:
|
||||||
|
await UniMessage.text("哇呀...查询出错了!").send(event)
|
||||||
|
return
|
||||||
|
|
||||||
|
msg = UniMessage.text(f"现在的节气是{date.term}")
|
||||||
|
await msg.send(event)
|
||||||
|
|
||||||
@ -8,7 +8,7 @@ import functools
|
|||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from nonebot import on_message
|
from nonebot import on_message
|
||||||
from nonebot.rule import KeywordsRule, Rule, ToMeRule
|
from nonebot.rule import KeywordsRule, Rule
|
||||||
from nonebot.adapters.onebot.v11.bot import Bot as OBBot
|
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.event import MessageEvent as OBMessageEvent
|
||||||
from nonebot.adapters.onebot.v11.message import MessageSegment as OBMessageSegment
|
from nonebot.adapters.onebot.v11.message import MessageSegment as OBMessageSegment
|
||||||
@ -21,9 +21,10 @@ from konabot.common.apis.ali_content_safety import AlibabaGreen
|
|||||||
|
|
||||||
|
|
||||||
keywords = ("szmtq", "tqszm", "提取首字母", "首字母提取", )
|
keywords = ("szmtq", "tqszm", "提取首字母", "首字母提取", )
|
||||||
|
rule = Rule(KeywordsRule(*keywords))
|
||||||
|
|
||||||
|
|
||||||
cmd_tqszm = on_message(rule=Rule(ToMeRule(), KeywordsRule(*keywords)))
|
cmd_tqszm = on_message(rule=rule)
|
||||||
|
|
||||||
@cmd_tqszm.handle()
|
@cmd_tqszm.handle()
|
||||||
async def _(target: DepLongTaskTarget, msg: UniMsg, evt: OBMessageEvent | None = None, bot: OBBot | None = None):
|
async def _(target: DepLongTaskTarget, msg: UniMsg, evt: OBMessageEvent | None = None, bot: OBBot | None = None):
|
||||||
|
|||||||
108
konabot/plugins/typst/__init__.py
Normal file
108
konabot/plugins/typst/__init__.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import asyncio
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
from nonebot import on_command
|
||||||
|
from nonebot.adapters import Event, Bot
|
||||||
|
from nonebot_plugin_alconna import UniMessage, UniMsg
|
||||||
|
from nonebot.adapters.onebot.v11.event import MessageEvent as OB11MessageEvent
|
||||||
|
from nonebot.adapters.onebot.v11.bot import Bot as OB11Bot
|
||||||
|
from nonebot.adapters.onebot.v11.message import Message as OB11Message
|
||||||
|
|
||||||
|
from konabot.common.longtask import DepLongTaskTarget
|
||||||
|
from konabot.common.path import TMP_PATH
|
||||||
|
|
||||||
|
|
||||||
|
TEMPLATE_PATH = Path(__file__).parent / "template.typ"
|
||||||
|
TEMPLATE = TEMPLATE_PATH.read_text()
|
||||||
|
|
||||||
|
|
||||||
|
def render_sync(code: str) -> bytes:
|
||||||
|
with TemporaryDirectory(dir=TMP_PATH) as tmpdirname:
|
||||||
|
temp_dir = Path(tmpdirname).resolve()
|
||||||
|
temp_typ = temp_dir / "page.typ"
|
||||||
|
temp_typ.write_text(TEMPLATE + "\n\n" + code)
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
"typst",
|
||||||
|
"compile",
|
||||||
|
temp_typ.name,
|
||||||
|
"--format",
|
||||||
|
"png",
|
||||||
|
"--root",
|
||||||
|
temp_dir,
|
||||||
|
"--ppi",
|
||||||
|
"300",
|
||||||
|
]
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd, capture_output=True, text=True, cwd=temp_dir.resolve(), timeout=50
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"渲染了 Typst "
|
||||||
|
f"STDOUT={result.stdout} "
|
||||||
|
f"STDERR={result.stderr} "
|
||||||
|
f"RETURNCODE={result.returncode}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise subprocess.CalledProcessError(
|
||||||
|
result.returncode, cmd, result.stdout, result.stderr
|
||||||
|
)
|
||||||
|
|
||||||
|
result_png = temp_dir / "page.png"
|
||||||
|
if not result_png.exists():
|
||||||
|
raise FileNotFoundError("Typst 没有输出图片文件")
|
||||||
|
|
||||||
|
return result_png.read_bytes()
|
||||||
|
|
||||||
|
|
||||||
|
async def render(code: str) -> bytes:
|
||||||
|
task = asyncio.to_thread(lambda: render_sync(code))
|
||||||
|
return await task
|
||||||
|
|
||||||
|
|
||||||
|
cmd = on_command("typst")
|
||||||
|
|
||||||
|
|
||||||
|
@cmd.handle()
|
||||||
|
async def _(evt: Event, bot: Bot, msg: UniMsg, target: DepLongTaskTarget):
|
||||||
|
typst_code = ""
|
||||||
|
if isinstance(evt, OB11MessageEvent):
|
||||||
|
if evt.reply is not None:
|
||||||
|
typst_code = evt.reply.message.extract_plain_text()
|
||||||
|
else:
|
||||||
|
for seg in evt.get_message():
|
||||||
|
if seg.type == "reply":
|
||||||
|
msgid = seg.get("id")
|
||||||
|
if msgid is not None:
|
||||||
|
msg2data = await cast(OB11Bot, bot).get_msg(message_id=msgid)
|
||||||
|
typst_code = OB11Message(
|
||||||
|
msg2data.get("message")
|
||||||
|
).extract_plain_text()
|
||||||
|
|
||||||
|
typst_code += msg.extract_plain_text().removeprefix("typst").strip()
|
||||||
|
|
||||||
|
if len(typst_code) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
res = await render(typst_code)
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
await target.send_message("渲染出错:内部错误")
|
||||||
|
raise e from e
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
await target.send_message("渲染出错,以下是输出消息:\n\n" + e.stderr)
|
||||||
|
return
|
||||||
|
except TimeoutError:
|
||||||
|
await target.send_message("渲染出错:渲染超时")
|
||||||
|
return
|
||||||
|
except PermissionError as e:
|
||||||
|
await target.send_message("渲染出错:内部错误")
|
||||||
|
raise e from e
|
||||||
|
|
||||||
|
await target.send_message(UniMessage.image(raw=res), at=False)
|
||||||
5
konabot/plugins/typst/template.typ
Normal file
5
konabot/plugins/typst/template.typ
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
#import "@preview/cetz:0.4.2"
|
||||||
|
|
||||||
|
#set page(width: auto, height: auto, margin: (x: 10pt, y: 10pt))
|
||||||
|
#set text(font: ("Noto Sans CJK SC"))
|
||||||
|
|
||||||
161
poetry.lock
generated
161
poetry.lock
generated
@ -569,6 +569,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 = "asyncio-dgram"
|
||||||
|
version = "3.0.0"
|
||||||
|
description = "Higher level Datagram support for Asyncio"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "asyncio_dgram-3.0.0-py3-none-any.whl", hash = "sha256:a4113061e6a7fbeee928d49c56cb61b68ca4a2fbee37f7e97280bbc72323ba8e"},
|
||||||
|
{file = "asyncio_dgram-3.0.0.tar.gz", hash = "sha256:bd0937807a44451d799573b32400702187fd0bba6136f7cf306e9327c0c20f3e"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.source]
|
||||||
|
type = "legacy"
|
||||||
|
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||||
|
reference = "mirrors"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "attrs"
|
name = "attrs"
|
||||||
version = "25.4.0"
|
version = "25.4.0"
|
||||||
@ -733,6 +750,22 @@ type = "legacy"
|
|||||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||||
reference = "mirrors"
|
reference = "mirrors"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "borax"
|
||||||
|
version = "4.1.3"
|
||||||
|
description = "A tool collections.(Chinese-Lunar-Calendars/Python-Patterns)"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "borax-4.1.3-py3-none-any.whl", hash = "sha256:bdba9abe1c3be4ba1b6a014b3b4a97a7ba254a08c7f40cd4428a13b84db02558"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.source]
|
||||||
|
type = "legacy"
|
||||||
|
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||||
|
reference = "mirrors"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "brotli"
|
name = "brotli"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@ -1284,6 +1317,32 @@ type = "legacy"
|
|||||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||||
reference = "mirrors"
|
reference = "mirrors"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dnspython"
|
||||||
|
version = "2.8.0"
|
||||||
|
description = "DNS toolkit"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af"},
|
||||||
|
{file = "dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["black (>=25.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.17.0)", "mypy (>=1.17)", "pylint (>=3)", "pytest (>=8.4)", "pytest-cov (>=6.2.0)", "quart-trio (>=0.12.0)", "sphinx (>=8.2.0)", "sphinx-rtd-theme (>=3.0.0)", "twine (>=6.1.0)", "wheel (>=0.45.0)"]
|
||||||
|
dnssec = ["cryptography (>=45)"]
|
||||||
|
doh = ["h2 (>=4.2.0)", "httpcore (>=1.0.0)", "httpx (>=0.28.0)"]
|
||||||
|
doq = ["aioquic (>=1.2.0)"]
|
||||||
|
idna = ["idna (>=3.10)"]
|
||||||
|
trio = ["trio (>=0.30)"]
|
||||||
|
wmi = ["wmi (>=1.5.1) ; platform_system == \"Windows\""]
|
||||||
|
|
||||||
|
[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"
|
||||||
@ -2253,6 +2312,27 @@ type = "legacy"
|
|||||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||||
reference = "mirrors"
|
reference = "mirrors"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mcstatus"
|
||||||
|
version = "12.2.1"
|
||||||
|
description = "A library to query Minecraft Servers for their status and capabilities."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "mcstatus-12.2.1-py3-none-any.whl", hash = "sha256:9da6b8b6c1a43521f13cc50f312501091c1e7437c3ad2d65b9ccb534d70b1244"},
|
||||||
|
{file = "mcstatus-12.2.1.tar.gz", hash = "sha256:0c6fb84c96685bb02189951ba895afbaca22b65bfe74e7fc55ef73a1c6e8a9ce"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
asyncio-dgram = ">=2.1.2"
|
||||||
|
dnspython = ">=2.4.2"
|
||||||
|
|
||||||
|
[package.source]
|
||||||
|
type = "legacy"
|
||||||
|
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||||
|
reference = "mirrors"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mdit-py-plugins"
|
name = "mdit-py-plugins"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@ -4046,6 +4126,85 @@ type = "legacy"
|
|||||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||||
reference = "mirrors"
|
reference = "mirrors"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shapely"
|
||||||
|
version = "2.1.2"
|
||||||
|
description = "Manipulation and analysis of geometric objects"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "shapely-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7ae48c236c0324b4e139bea88a306a04ca630f49be66741b340729d380d8f52f"},
|
||||||
|
{file = "shapely-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eba6710407f1daa8e7602c347dfc94adc02205ec27ed956346190d66579eb9ea"},
|
||||||
|
{file = "shapely-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef4a456cc8b7b3d50ccec29642aa4aeda959e9da2fe9540a92754770d5f0cf1f"},
|
||||||
|
{file = "shapely-2.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e38a190442aacc67ff9f75ce60aec04893041f16f97d242209106d502486a142"},
|
||||||
|
{file = "shapely-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:40d784101f5d06a1fd30b55fc11ea58a61be23f930d934d86f19a180909908a4"},
|
||||||
|
{file = "shapely-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f6f6cd5819c50d9bcf921882784586aab34a4bd53e7553e175dece6db513a6f0"},
|
||||||
|
{file = "shapely-2.1.2-cp310-cp310-win32.whl", hash = "sha256:fe9627c39c59e553c90f5bc3128252cb85dc3b3be8189710666d2f8bc3a5503e"},
|
||||||
|
{file = "shapely-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:1d0bfb4b8f661b3b4ec3565fa36c340bfb1cda82087199711f86a88647d26b2f"},
|
||||||
|
{file = "shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91121757b0a36c9aac3427a651a7e6567110a4a67c97edf04f8d55d4765f6618"},
|
||||||
|
{file = "shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a9c722ba774cf50b5d4541242b4cce05aafd44a015290c82ba8a16931ff63d"},
|
||||||
|
{file = "shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc4f7397459b12c0b196c9efe1f9d7e92463cbba142632b4cc6d8bbbbd3e2b09"},
|
||||||
|
{file = "shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136ab87b17e733e22f0961504d05e77e7be8c9b5a8184f685b4a91a84efe3c26"},
|
||||||
|
{file = "shapely-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:16c5d0fc45d3aa0a69074979f4f1928ca2734fb2e0dde8af9611e134e46774e7"},
|
||||||
|
{file = "shapely-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ddc759f72b5b2b0f54a7e7cde44acef680a55019eb52ac63a7af2cf17cb9cd2"},
|
||||||
|
{file = "shapely-2.1.2-cp311-cp311-win32.whl", hash = "sha256:2fa78b49485391224755a856ed3b3bd91c8455f6121fee0db0e71cefb07d0ef6"},
|
||||||
|
{file = "shapely-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:c64d5c97b2f47e3cd9b712eaced3b061f2b71234b3fc263e0fcf7d889c6559dc"},
|
||||||
|
{file = "shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94"},
|
||||||
|
{file = "shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359"},
|
||||||
|
{file = "shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3"},
|
||||||
|
{file = "shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b"},
|
||||||
|
{file = "shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc"},
|
||||||
|
{file = "shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d"},
|
||||||
|
{file = "shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454"},
|
||||||
|
{file = "shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179"},
|
||||||
|
{file = "shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8"},
|
||||||
|
{file = "shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a"},
|
||||||
|
{file = "shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e"},
|
||||||
|
{file = "shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6"},
|
||||||
|
{file = "shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af"},
|
||||||
|
{file = "shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd"},
|
||||||
|
{file = "shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350"},
|
||||||
|
{file = "shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715"},
|
||||||
|
{file = "shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40"},
|
||||||
|
{file = "shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b"},
|
||||||
|
{file = "shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801"},
|
||||||
|
{file = "shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0"},
|
||||||
|
{file = "shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c"},
|
||||||
|
{file = "shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99"},
|
||||||
|
{file = "shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf"},
|
||||||
|
{file = "shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c"},
|
||||||
|
{file = "shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223"},
|
||||||
|
{file = "shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c"},
|
||||||
|
{file = "shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df"},
|
||||||
|
{file = "shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf"},
|
||||||
|
{file = "shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4"},
|
||||||
|
{file = "shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc"},
|
||||||
|
{file = "shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566"},
|
||||||
|
{file = "shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c"},
|
||||||
|
{file = "shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a"},
|
||||||
|
{file = "shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076"},
|
||||||
|
{file = "shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1"},
|
||||||
|
{file = "shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0"},
|
||||||
|
{file = "shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26"},
|
||||||
|
{file = "shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0"},
|
||||||
|
{file = "shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735"},
|
||||||
|
{file = "shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9"},
|
||||||
|
{file = "shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
numpy = ">=1.21"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["matplotlib", "numpydoc (==1.1.*)", "sphinx", "sphinx-book-theme", "sphinx-remove-toctrees"]
|
||||||
|
test = ["pytest", "pytest-cov", "scipy-doctest"]
|
||||||
|
|
||||||
|
[package.source]
|
||||||
|
type = "legacy"
|
||||||
|
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||||
|
reference = "mirrors"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "skia-python"
|
name = "skia-python"
|
||||||
version = "138.0"
|
version = "138.0"
|
||||||
@ -4983,4 +5142,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 = "59498c038a603c90f051d2f360cb9226ec0fc4470942c0a7cf34f832701f0ce7"
|
content-hash = "15e51d7d14d091295e7d0ecabaa601fd65ae392fce28f90d5f3deb4718544e17"
|
||||||
|
|||||||
@ -31,6 +31,9 @@ dependencies = [
|
|||||||
"sqlparse (>=0.5.0,<1.0.0)",
|
"sqlparse (>=0.5.0,<1.0.0)",
|
||||||
"alibabacloud-green20220302 (>=3.0.1,<4.0.0)",
|
"alibabacloud-green20220302 (>=3.0.1,<4.0.0)",
|
||||||
"pypinyin (>=0.55.0,<0.56.0)",
|
"pypinyin (>=0.55.0,<0.56.0)",
|
||||||
|
"shapely (>=2.1.2,<3.0.0)",
|
||||||
|
"mcstatus (>=12.2.1,<13.0.0)",
|
||||||
|
"borax (>=4.1.3,<5.0.0)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
|
|||||||
@ -5,7 +5,13 @@ def main():
|
|||||||
with playwright.sync_api.sync_playwright() as p:
|
with playwright.sync_api.sync_playwright() as p:
|
||||||
browser = p.chromium.launch()
|
browser = p.chromium.launch()
|
||||||
page = browser.new_page()
|
page = browser.new_page()
|
||||||
page.goto("https://www.baidu.com")
|
content = page.content()
|
||||||
|
if "<html" in content or len(content) < 100:
|
||||||
|
print("✅ Playwright + Chromium 环境正常 (在 about:blank 页面上测试成功)")
|
||||||
|
else:
|
||||||
|
print("⚠️ Playwright + Chromium 环境启动,但页面内容检查结果异常。")
|
||||||
|
print(content)
|
||||||
|
raise Exception()
|
||||||
print("Playwright + Chromium 环境正常")
|
print("Playwright + Chromium 环境正常")
|
||||||
browser.close()
|
browser.close()
|
||||||
|
|
||||||
|
|||||||
@ -12,5 +12,8 @@ def filter(change: Change, path: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
if Path(path).absolute().is_relative_to((base / ".git").absolute()):
|
if Path(path).absolute().is_relative_to((base / ".git").absolute()):
|
||||||
return False
|
return False
|
||||||
|
if Path(path).absolute().is_relative_to((base / "assets" / "oracle" / "image").absolute()):
|
||||||
|
# 还要解决坏枪的这个问题
|
||||||
|
return False
|
||||||
print(path)
|
print(path)
|
||||||
return True
|
return True
|
||||||
|
|||||||
Reference in New Issue
Block a user