Compare commits

...

18 Commits

Author SHA1 Message Date
379e677bea No drone anymore!!! I will deploy by myself!!! 2026-06-09 14:54:06 +08:00
1d89d80676 调整 AI 模型不要思考,以及添加 Wolfx 日志详细程度
Some checks reported errors
continuous-integration/drone/push Build was killed
2026-06-09 14:49:37 +08:00
9265c250b3 补充一些权限系统有关的注释
All checks were successful
continuous-integration/drone/push Build is passing
2026-05-20 19:34:12 +08:00
4dd9320678 取消罗文的反应机制
All checks were successful
continuous-integration/drone/push Build is passing
2026-05-20 19:11:46 +08:00
db96202d5d 添加小睦想
All checks were successful
continuous-integration/drone/push Build is passing
2026-05-19 00:11:32 +08:00
881b08c41f 此方晚安文档更新
All checks were successful
continuous-integration/drone/push Build is passing
2026-05-13 15:33:40 +08:00
c11d29e136 Merge branch 'master' of ssh://gitea.service.jazzwhom.top:2221/mttu-developers/konabot
Some checks failed
continuous-integration/drone/push Build is failing
2026-05-13 15:25:25 +08:00
1fa74b61d6 更新各种依赖 2026-05-13 15:25:11 +08:00
f0601acbe9 oyasumi
All checks were successful
continuous-integration/drone/push Build is passing
2026-05-13 15:06:49 +08:00
39c7c043ca Merge branch 'master' of ssh://gitea.service.jazzwhom.top:2221/mttu-developers/konabot
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-29 18:52:01 +08:00
39accb16e0 桂花说 2026-04-29 18:51:37 +08:00
ec1f9627f3 Merge pull request 'chores: manual fix and pbr pipeline' (#73) from bkbkzzzz/konabot:marchtoy_gl into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #73
2026-04-28 14:11:47 +08:00
c0590dacbc Merge branch 'master' into marchtoy_gl 2026-04-28 14:11:08 +08:00
d748e242db manual fix 2026-04-28 14:09:45 +08:00
e2fd0809a5 PBR 2026-04-28 01:09:17 +08:00
2144b1e0eb 补充解压 Typst 构建产物需要的依赖
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-28 00:39:37 +08:00
7d0d53bead 修复并调整构建产物的生命周期
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-28 00:35:03 +08:00
4bfcc9b41c Merge pull request '修复偶发的数据库连接失效问题' (#72) from fix/database-lock into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #72
2026-04-28 00:12:44 +08:00
19 changed files with 1116 additions and 914 deletions

View File

@ -1,105 +0,0 @@
---
kind: pipeline
name: 构建 Docker Nightly 镜像
type: docker
trigger:
event:
- push
branch:
- master
steps:
- name: submodules
image: alpine/git
commands:
- git submodule update --init --recursive
- name: 构建 Docker 镜像
image: plugins/docker:latest
privileged: true
settings:
username: kagami-ci
password:
from_secret: KAGAMI-CI-PASSWORD
repo: gitea.service.jazzwhom.top/mttu-developers/konabot
registry: gitea.service.jazzwhom.top
tags:
- nightly
- nightly-${DRONE_COMMIT_SHA}
dockerfile: Dockerfile
volumes:
- name: docker-socket
path: /var/run/docker.sock
- name: 在容器中进行若干测试
image: docker:dind
privileged: true
volumes:
- name: docker-socket
path: /var/run/docker.sock
commands:
- docker run --rm gitea.service.jazzwhom.top/mttu-developers/konabot:nightly-${DRONE_COMMIT_SHA} python scripts/test_plugin_load.py
- docker run --rm gitea.service.jazzwhom.top/mttu-developers/konabot:nightly-${DRONE_COMMIT_SHA} python scripts/test_playwright.py
- docker run --rm gitea.service.jazzwhom.top/mttu-developers/konabot:nightly-${DRONE_COMMIT_SHA} python -m pytest --cov=./konabot/ --cov-report term-missing:skip-covered
- name: 发送构建结果到 ntfy
image: parrazam/drone-ntfy
when:
status: [success, failure]
settings:
url: https://ntfy.service.jazzwhom.top
topic: drone_ci
tags:
- drone-ci
token:
from_secret: NTFY_TOKEN
volumes:
- name: docker-socket
host:
path: /var/run/docker.sock
---
kind: pipeline
name: 构建 Docker Release 镜像
type: docker
trigger:
event:
- tag
steps:
- name: submodules
image: alpine/git
commands:
- git submodule update --init --recursive
- name: 构建并推送 Release Docker 镜像
image: plugins/docker:latest
privileged: true
settings:
username: kagami-ci
password:
from_secret: KAGAMI-CI-PASSWORD
repo: gitea.service.jazzwhom.top/mttu-developers/konabot
registry: gitea.service.jazzwhom.top
tags:
- ${DRONE_TAG}
- latest
dockerfile: Dockerfile
volumes:
- name: docker-socket
path: /var/run/docker.sock
- name: 发送构建结果到 ntfy
image: parrazam/drone-ntfy
when:
status: [success, failure]
settings:
url: https://ntfy.service.jazzwhom.top
topic: drone_ci
tags:
- drone-ci
token:
from_secret: NTFY_TOKEN
volumes:
- name: docker-socket
host:
path: /var/run/docker.sock

View File

@ -5,6 +5,8 @@ ENV VIRTUAL_ENV=/app/.venv \
PLAYWRIGHT_BROWSERS_PATH=/usr/lib/pw-browsers PLAYWRIGHT_BROWSERS_PATH=/usr/lib/pw-browsers
# 安装所有都需要的底层依赖 # 安装所有都需要的底层依赖
#
# xz-utils: 解压需要它
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
libfontconfig1 libgl1 libegl1 libglvnd0 mesa-vulkan-drivers at-spi2-common fontconfig \ libfontconfig1 libgl1 libegl1 libglvnd0 mesa-vulkan-drivers at-spi2-common fontconfig \
@ -16,6 +18,7 @@ RUN apt-get update && \
libatk-bridge2.0-0t64 libatspi2.0-0t64 libxcomposite1 libxdamage1 libxfixes3 \ libatk-bridge2.0-0t64 libatspi2.0-0t64 libxcomposite1 libxdamage1 libxfixes3 \
libxkbcommon0 libasound2t64 libnss3 fonts-noto-cjk fonts-noto-cjk-extra \ libxkbcommon0 libasound2t64 libnss3 fonts-noto-cjk fonts-noto-cjk-extra \
fonts-noto-color-emoji \ fonts-noto-color-emoji \
xz-utils \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*

BIN
assets/img/meme/xiaomu.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

View File

@ -4,7 +4,7 @@ Wolfx 防灾免费 API
import asyncio import asyncio
import json import json
from typing import Literal, TypeVar, cast from typing import TypeVar, cast
import aiohttp import aiohttp
from aiosignal import Signal from aiosignal import Signal
from loguru import logger from loguru import logger
@ -239,7 +239,8 @@ class WolfxAPIService:
logger.info(f"接收到来自 Wolfx API 的信息:{data}") logger.info(f"接收到来自 Wolfx API 的信息:{data}")
await signal.send(obj) await signal.send(obj)
except pydantic.ValidationError as e: except pydantic.ValidationError as e:
logger.warning(f"解析 Wolfx API 时出错 URL={ws.url}") data_text = data.decode('utf-8', 'replace')
logger.warning(f"解析 Wolfx API 时出错 URL={ws.url} raw={data_text}")
logger.error(e) logger.error(e)
async def start(self): # pragma: no cover async def start(self): # pragma: no cover

View File

@ -50,7 +50,14 @@ class ArtifactDepends:
tasks = set() tasks = set()
for f in self.callbacks: for f in self.callbacks:
tasks.add(f(downloaded)) tasks.add(f(downloaded))
return await asyncio.gather(*tasks, return_exceptions=True) result = await asyncio.gather(*tasks, return_exceptions=True)
for r in result:
if isinstance(r, BaseException):
logger.warning("完成了二进制文件的下载,但是有未捕捉的错误")
logger.exception(r)
return result
class Config(BaseModel): class Config(BaseModel):
@ -73,12 +80,7 @@ async def _():
async def _task(artifact: ArtifactDepends): async def _task(artifact: ArtifactDepends):
async with semaphore: async with semaphore:
downloaded = await ensure_artifact(artifact) await ensure_artifact(artifact)
result = await artifact._finished(downloaded)
for r in result:
if isinstance(r, BaseException):
logger.warning("完成了二进制文件的下载,但是有未捕捉的错误")
logger.exception(r)
tasks: set[asyncio.Task] = set() tasks: set[asyncio.Task] = set()
for a in artifact_list: for a in artifact_list:
@ -116,9 +118,16 @@ async def download_artifact(artifact: ArtifactDepends):
f"下载到的二进制的 sha256 与需求不同 TARGET={artifact.target} REQUESTED={artifact.sha256} ACTUAL={m.hexdigest()}" f"下载到的二进制的 sha256 与需求不同 TARGET={artifact.target} REQUESTED={artifact.sha256} ACTUAL={m.hexdigest()}"
) )
await artifact._finished(True)
async def ensure_artifact(artifact: ArtifactDepends) -> bool: async def ensure_artifact(artifact: ArtifactDepends) -> bool:
"""
确保所需的二进制存在。返回是否下载了这个二进制文件。
"""
if not artifact.is_corresponding_platform(): if not artifact.is_corresponding_platform():
logger.debug(f"所需求的平台不是当前平台,跳过二进制下载 artifact={artifact}")
return False return False
if not artifact.target.exists(): if not artifact.target.exists():
@ -136,6 +145,7 @@ async def ensure_artifact(artifact: ArtifactDepends) -> bool:
artifact.target.unlink() artifact.target.unlink()
await download_artifact(artifact) await download_artifact(artifact)
return True return True
await artifact._finished(False)
return False return False

View File

@ -29,10 +29,21 @@ async def _to_entity_chain(el: _EntityLike):
class PermManager: class PermManager:
"""
权限管理模块
"""
def __init__(self, db: DatabaseManager) -> None: def __init__(self, db: DatabaseManager) -> None:
self.db = db self.db = db
async def get_permission_info(self, entities: _EntityLike, key: str): async def get_permission_info(
self, entities: _EntityLike, key: str
) -> tuple[PermEntity, str, bool] | None:
"""
获得一个权限实体或权限实体串对一个 key 的权限信息。若未入库(默认值)则
代表没有该权限相关的记录
"""
entities = await _to_entity_chain(entities) entities = await _to_entity_chain(entities)
key = key.removesuffix("*").removesuffix(".") key = key.removesuffix("*").removesuffix(".")
key_split = key.split(".") key_split = key.split(".")
@ -52,17 +63,29 @@ class PermManager:
return None return None
async def check_has_permission(self, entities: _EntityLike, key: str) -> bool: async def check_has_permission(self, entities: _EntityLike, key: str) -> bool:
"""
检查一个权限实体或者权限实体串是否有权限
"""
res = await self.get_permission_info(entities, key) res = await self.get_permission_info(entities, key)
if res is None: if res is None:
return False return False
return res[2] return res[2]
async def update_permission(self, entity: PermEntity, key: str, perm: bool | None): async def update_permission(self, entity: PermEntity, key: str, perm: bool | None):
"""
更新一个具体的权限实体的一则权限
"""
async with self.db.get_conn() as conn: async with self.db.get_conn() as conn:
repo = PermRepo(conn) repo = PermRepo(conn)
await repo.update_perm_info(entity, key, perm) await repo.update_perm_info(entity, key, perm)
async def list_permission(self, entities: _EntityLike, query: PagerQuery): async def list_permission(self, entities: _EntityLike, query: PagerQuery):
"""
列出一个权限实体或权限实体串拥有的所有权限记录
"""
entities = await _to_entity_chain(entities) entities = await _to_entity_chain(entities)
async with self.db.get_conn() as conn: async with self.db.get_conn() as conn:
repo = PermRepo(conn) repo = PermRepo(conn)
@ -113,6 +136,22 @@ def register_default_allow_permission(key: str):
def require_permission(perm: str) -> Rule: # pragma: no cover def require_permission(perm: str) -> Rule: # pragma: no cover
"""
`require_permission` 是一个 Nonebot 规则,可以用来要求一个 Nonebot 的指令需
要拥有一定的权限。
```python
from konabot.common.permsys import require_permission
from nonebot import on_command
cmd = on_command("kz", rule=require_permission("kagami.kz"))
@cmd.handle()
async def _():
await cmd.finish("你抓到了普通pt")
```
"""
async def check_permission(event: Event, pm: DepPermManager) -> bool: async def check_permission(event: Event, pm: DepPermManager) -> bool:
return await pm.check_has_permission(event, perm) return await pm.check_has_permission(event, perm)

View File

@ -22,6 +22,11 @@ class PermEntity:
def get_entity_chain_of_entity(entity: PermEntity) -> list[PermEntity]: def get_entity_chain_of_entity(entity: PermEntity) -> list[PermEntity]:
"""
获得一个权限实体的权限串。实际上返回三个权限,从小到大分别是用户、平台全体和
系统全局的权限实体。
"""
return [ return [
PermEntity("sys", "global", "global"), PermEntity("sys", "global", "global"),
PermEntity(entity.platform, "global", "global"), PermEntity(entity.platform, "global", "global"),
@ -30,6 +35,10 @@ def get_entity_chain_of_entity(entity: PermEntity) -> list[PermEntity]:
async def get_entity_chain(event: Event) -> list[PermEntity]: # pragma: no cover async def get_entity_chain(event: Event) -> list[PermEntity]: # pragma: no cover
"""
获得一个 Nonebot Event 的权限实体串。
"""
entities = [PermEntity("sys", "global", "global")] entities = [PermEntity("sys", "global", "global")]
if isinstance(event, OB11Event): if isinstance(event, OB11Event):

View File

@ -1,3 +0,0 @@
# 关于罗文和洛温
AdoreLowen 希望和洛温阿特金森区分,所以最好就不要叫他洛温了!此方 BOT 会在一些群提醒叫错了的人。

View File

@ -1,16 +1,16 @@
# 指令介绍 # 指令介绍
简易 Raymarch 小玩具 简易 Raymarch 小玩具
用法march `<scene>` 用法march `[scene]`
march torus(1.0, 0.2).color(1.0, 0.2, 0.2) torus(1.0, 0.2).rot(90, 0, 0).color(0.2, 0.2, 1.0) camera(4.0).pos(4, 0, 0).lookat(0) march torus(1.0, 0.2).color(1.0, 0.2, 0.2) torus(1.0, 0.2).rot(90, 0, 0).color(0.2, 0.2, 1.0) camera(4.0).pos(4, 0, 0).lookat(0)
# 主要语法 # 主要语法
`<scene>` ::= `<scene>` "." `<op>` |`<obj>` `[scene]` ::= `[scene]` "." `[op]` |`[obj]`
`<obj>` ::= `<obj_ty>` | `<obj_ty>` "(" <args> ")" `[obj]` ::= `[obj_ty]` | `[obj_ty]` "(" [args] ")"
`<op>` ::= `<op_ty>` | `<op_ty>` "(" `<args>` ")" `[op]` ::= `[op_ty]` | `[op_ty]` "(" `[args]` ")"
`<args>` ::= `<args>` "," `<arg>` | `<arg>` `[args]` ::= `[args]` "," `[arg]` | `[arg]`
其中 `obj_ty`、`op_ty` 分别为物体类型(如 `cube`、`sphere`、`torus` 等)与变换类型(如 `pos`、`rot`)。 其中 `obj_ty`、`op_ty` 分别为物体类型(如 `cube`、`sphere`、`torus` 等)与变换类型(如 `pos`、`rot`)。
@ -37,4 +37,4 @@
`rounded`:圆角 `rounded`:圆角
# 特殊说明 # 特殊说明
`<op_ty>` 不包含 scale。非正交的变换会破坏 SDF 的性质。 `[op_ty]` 不包含 scale。非正交的变换会破坏 SDF 的性质。

View File

@ -0,0 +1,8 @@
# 指令介绍
**此方晚安** - 让此方 BOT 禁言你一段时间
## 指令格式
- `@此方BOT 此方晚安`: 禁言几个小时
- `@此方BOT 此方午安`: 禁言几十分钟

View File

@ -0,0 +1,44 @@
import re
from typing import Any
from nonebot import on_message
from nonebot.adapters import Event
from nonebot_plugin_alconna import UniMessage, UniMsg
from playwright.async_api import Page
from konabot.common.nb import match_keyword
from konabot.common.web_render import WebRenderer, konaweb
async def render_image(message: str) -> UniMessage[Any]:
"""
渲染文本为图片
"""
async def page_function(page: Page):
await page.wait_for_function("typeof setContent === 'function'")
await page.evaluate(
"([ message ]) => { return setContent(message); }",
[ message ],
)
img_data = await WebRenderer.render(
url=konaweb("guihuasay"),
target="#main",
other_function=page_function,
)
return UniMessage.image(raw=img_data)
cmd = on_message(
rule=match_keyword.match_keyword(
re.compile(r"^(桂花[说想])\s.+", re.I),
),
)
@cmd.handle()
async def _(event: Event, msg: UniMsg):
text = msg.extract_plain_text().lstrip()
_, content = text.split(maxsplit=1)
msg = await render_image(content)
await msg.send(event)

View File

@ -23,7 +23,7 @@ class THQwen(TextHandler):
ostream="你或当前环境没有使用 qwen 的权限。如有疑问请联系管理员", ostream="你或当前环境没有使用 qwen 的权限。如有疑问请联系管理员",
) )
llm = get_llm() llm = get_llm(llm_model="qwen3.7-plus")
messages = [] messages = []
if istream is not None: if istream is not None:
@ -48,7 +48,9 @@ class THQwen(TextHandler):
"content": "除非用户要求,请尽可能短点回答。另外,当前环境不支持 Markdown 语法,如果可以,请使用纯文本回答", "content": "除非用户要求,请尽可能短点回答。另外,当前环境不支持 Markdown 语法,如果可以,请使用纯文本回答",
} }
] + messages ] + messages
result = await llm.chat(cast(Any, messages)) result = await llm.chat(
cast(Any, messages), extra_body={"enable_thinking": False}
)
content = result.content content = result.content
if content is None: if content is None:
return TextHandleResult( return TextHandleResult(

View File

@ -2,6 +2,7 @@
const float EPS = 0.001; const float EPS = 0.001;
const int MAX_ITER = 128; const int MAX_ITER = 128;
const float INF = 1e10; const float INF = 1e10;
const float PI = 3.14159;
uniform vec2 u_resolution; uniform vec2 u_resolution;
out vec4 fragColor; out vec4 fragColor;
@ -68,17 +69,76 @@ vec3 nrm(vec3 p) {
)); ));
} }
float saturate(float x) {
return clamp(x, 0.0, 1.0);
}
float ggx_distribution(vec3 n, vec3 h, float roughness) {
float alpha = roughness * roughness;
float alpha2 = alpha * alpha;
float NdotH = saturate(dot(n, h));
float denom = NdotH * NdotH * (alpha2 - 1.0) + 1.0;
return alpha2 / max(PI * denom * denom, EPS);
}
float geometry_schlick_ggx(float NdotX, float roughness) {
float r = roughness + 1.0;
float k = r * r / 8.0;
return NdotX / max(NdotX * (1.0 - k) + k, EPS);
}
float geometry_smith(vec3 n, vec3 v, vec3 l, float roughness) {
float NdotV = saturate(dot(n, v));
float NdotL = saturate(dot(n, l));
return geometry_schlick_ggx(NdotV, roughness) * geometry_schlick_ggx(NdotL, roughness);
}
vec3 fresnel_schlick(vec3 f0, float cos_theta) {
return f0 + (1.0 - f0) * pow(1.0 - saturate(cos_theta), 5.0);
}
vec3 tonemap_aces(vec3 c) {
const float a = 2.51;
const float b = 0.03;
const float c1 = 2.43;
const float d = 0.59;
const float e = 0.14;
return clamp((c * (a * c + b)) / (c * (c1 * c + d) + e), 0.0, 1.0);
}
vec4 materialColor(int obj_id) { vec4 materialColor(int obj_id) {
<COLOR_BLOCK> <COLOR_BLOCK>
return vec4(1.0); return vec4(1.0);
} }
vec4 color(vec3 p, int obj_id) { vec4 color(vec3 p, vec3 r, int obj_id) {
vec3 normal = nrm(p); vec3 light_col = vec3(1.0);
vec3 light_dir = normalize(vec3(0.5, 0.8, -0.6)); vec4 albedo = materialColor(obj_id);
float light = 0.2 + 0.8 * max(dot(normal, light_dir), 0.0); vec3 N = nrm(p);
vec4 base = materialColor(obj_id); vec3 V = normalize(-r);
return vec4(base.rgb * light, base.a); vec3 L = normalize(vec3(0.5, 0.8, -0.6));
vec3 H = normalize(V + L);
float roughness = 0.45;
float metallic = 0.02;
float NdotL = saturate(dot(N, L));
float NdotV = saturate(dot(N, V));
float D = ggx_distribution(N, H, roughness);
float G = geometry_smith(N, V, L, roughness);
vec3 F0 = mix(vec3(0.04), albedo.rgb, metallic);
vec3 F = fresnel_schlick(F0, dot(V, H));
vec3 kD = (1.0 - F) * (1.0 - metallic);
vec3 diffuse = kD * albedo.rgb / PI;
vec3 specular = D * G * F / max(4.0 * NdotL * NdotV, EPS);
float hemi = N.y * 0.5 + 0.5;
vec3 sky_ambient = vec3(0.60, 0.72, 0.92);
vec3 ground_ambient = vec3(0.18, 0.16, 0.14);
vec3 ambient = mix(ground_ambient, sky_ambient, hemi) * (diffuse + 0.25 * F0) * 0.35;
vec3 col = ambient + (diffuse + specular) * light_col * NdotL;
col = tonemap_aces(col);
col = pow(col, vec3(1.0 / 2.2));
return vec4(col, 1.0);
} }
vec4 march(vec3 p, vec3 r) { vec4 march(vec3 p, vec3 r) {
@ -87,7 +147,7 @@ vec4 march(vec3 p, vec3 r) {
for(int i = 0; i < MAX_ITER; ++i) { for(int i = 0; i < MAX_ITER; ++i) {
qry = sd(p); qry = sd(p);
if(qry.value < EPS){ if(qry.value < EPS){
col = color(p, qry.obj_id); col = color(p, r, qry.obj_id);
break; break;
} }
p += qry.value * r; p += qry.value * r;

View File

@ -1,6 +1,8 @@
from io import BytesIO from io import BytesIO
from typing import Iterable, cast from typing import Iterable, cast
import PIL.Image
from loguru import logger from loguru import logger
from nonebot import on_message from nonebot import on_message
from nonebot_plugin_alconna import ( from nonebot_plugin_alconna import (
@ -18,7 +20,7 @@ from nonebot_plugin_alconna import (
from playwright.async_api import ConsoleMessage, Page from playwright.async_api import ConsoleMessage, Page
from konabot.common.nb.match_keyword import match_keyword from konabot.common.nb.match_keyword import match_keyword
from konabot.common.nb.extract_image import DepPILImage from konabot.common.nb.extract_image import DepImageBytesOrNone, DepPILImage
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
from konabot.common.web_render.host_images import host_tempdir from konabot.common.web_render.host_images import host_tempdir
@ -35,6 +37,7 @@ from konabot.plugins.memepack.drawing.saying import (
draw_pt, draw_pt,
draw_suan, draw_suan,
draw_vr, draw_vr,
draw_xm
) )
from konabot.plugins.memepack.drawing.watermark import draw_doubao_watermark from konabot.plugins.memepack.drawing.watermark import draw_doubao_watermark
@ -361,3 +364,33 @@ async def _(saying: list[str]):
await vrsay.send(await UniMessage().image(raw=img_bytes).export()) await vrsay.send(await UniMessage().image(raw=img_bytes).export())
xmsay = on_alconna(
Alconna(
"小睦说",
Args[
"saying",
MultiVar(str, "*"),
Field(missing_tips=lambda: "你没有写小睦说了什么"),
],
Args["image?", Image | None],
),
use_cmd_start=True,
use_cmd_sep=False,
skip_for_unmatch=False,
aliases={"小睦想"},
)
@xmsay.handle()
async def _(saying: list[str], image: DepImageBytesOrNone):
if image is not None:
img = PIL.Image.open(BytesIO(image))
else:
img = None
img = await draw_xm("\n".join(saying), img)
img_bytes = BytesIO()
img.save(img_bytes, format="PNG")
await xmsay.send(await UniMessage().image(raw=img_bytes).export())

View File

@ -7,23 +7,41 @@ import PIL.Image
from konabot.common.path import ASSETS_PATH from konabot.common.path import ASSETS_PATH
from konabot.common.utils.to_async import make_async from konabot.common.utils.to_async import make_async
from .base.fonts import HARMONYOS_SANS_SC_BLACK, HARMONYOS_SANS_SC_REGULAR, LXGWWENKAI_REGULAR from .base.fonts import (
HARMONYOS_SANS_SC_BLACK,
HARMONYOS_SANS_SC_REGULAR,
LXGWWENKAI_REGULAR,
)
geimao_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "geimao.jpg").convert("RGBA") geimao_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "geimao.jpg").convert(
"RGBA"
)
pt_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "ptsay.png").convert("RGBA") pt_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "ptsay.png").convert("RGBA")
mnk_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "mnksay.jpg").convert("RGBA") mnk_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "mnksay.jpg").convert("RGBA")
dasuan_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "dss.png").convert("RGBA") dasuan_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "dss.png").convert("RGBA")
suan_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "suanleba.png").convert("RGBA") suan_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "suanleba.png").convert(
cute_ten_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "tententen.png").convert("RGBA") "RGBA"
)
cute_ten_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "tententen.png").convert(
"RGBA"
)
kio_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "kiosay.jpg").convert("RGBA") kio_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "kiosay.jpg").convert("RGBA")
vr_image = PIL.Image.open(ASSETS_PATH / 'img' / 'meme' / 'vr.jpg').convert("RGBA") vr_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "vr.jpg").convert("RGBA")
xm_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "xiaomu.png").convert("RGBA")
def _draw_geimao(saying: str): def _draw_geimao(saying: str):
img = geimao_image.copy() img = geimao_image.copy()
with imagetext_py.Writer(img) as iw: with imagetext_py.Writer(img) as iw:
iw.draw_text_wrapped( iw.draw_text_wrapped(
saying, 960, 50, 0.5, 0, 1920, 240, HARMONYOS_SANS_SC_BLACK, saying,
960,
50,
0.5,
0,
1920,
240,
HARMONYOS_SANS_SC_BLACK,
imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")), imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")),
0.8, 0.8,
imagetext_py.TextAlign.Center, imagetext_py.TextAlign.Center,
@ -42,7 +60,14 @@ def _draw_pt(saying: str):
img = pt_image.copy() img = pt_image.copy()
with imagetext_py.Writer(img) as iw: with imagetext_py.Writer(img) as iw:
iw.draw_text_wrapped( iw.draw_text_wrapped(
saying, 259, 278, 0.5, 0.5, 360, 48, HARMONYOS_SANS_SC_REGULAR, saying,
259,
278,
0.5,
0.5,
360,
48,
HARMONYOS_SANS_SC_REGULAR,
imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")), imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")),
1.0, 1.0,
imagetext_py.TextAlign.Center, imagetext_py.TextAlign.Center,
@ -59,7 +84,14 @@ def _draw_mnk(saying: str):
img = mnk_image.copy() img = mnk_image.copy()
with imagetext_py.Writer(img) as iw: with imagetext_py.Writer(img) as iw:
iw.draw_text_wrapped( iw.draw_text_wrapped(
saying, 540, 25, 0.5, 0, 1080, 120, HARMONYOS_SANS_SC_BLACK, saying,
540,
25,
0.5,
0,
1080,
120,
HARMONYOS_SANS_SC_BLACK,
imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")), imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")),
0.8, 0.8,
imagetext_py.TextAlign.Center, imagetext_py.TextAlign.Center,
@ -81,7 +113,14 @@ def _draw_suan(saying: str, dasuan: bool = False):
img = suan_image.copy() img = suan_image.copy()
with imagetext_py.Writer(img) as iw: with imagetext_py.Writer(img) as iw:
iw.draw_text_wrapped( iw.draw_text_wrapped(
saying, 1020, 290, 0.5, 0.5, 400, 48, LXGWWENKAI_REGULAR, saying,
1020,
290,
0.5,
0.5,
400,
48,
LXGWWENKAI_REGULAR,
imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")), imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")),
1.0, 1.0,
imagetext_py.TextAlign.Center, imagetext_py.TextAlign.Center,
@ -98,7 +137,14 @@ def _draw_cute_ten(saying: str):
img = cute_ten_image.copy() img = cute_ten_image.copy()
with imagetext_py.Writer(img) as iw: with imagetext_py.Writer(img) as iw:
iw.draw_text_wrapped( iw.draw_text_wrapped(
saying, 390, 479, 0.5, 0.5, 760, 96, LXGWWENKAI_REGULAR, saying,
390,
479,
0.5,
0.5,
760,
96,
LXGWWENKAI_REGULAR,
imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")), imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")),
1.0, 1.0,
imagetext_py.TextAlign.Center, imagetext_py.TextAlign.Center,
@ -116,7 +162,14 @@ def draw_kiosay(saying: str):
img = kio_image.copy() img = kio_image.copy()
with imagetext_py.Writer(img) as iw: with imagetext_py.Writer(img) as iw:
iw.draw_text_wrapped( iw.draw_text_wrapped(
saying, 450, 540, 0.5, 0.5, 900, 96, LXGWWENKAI_REGULAR, saying,
450,
540,
0.5,
0.5,
900,
96,
LXGWWENKAI_REGULAR,
imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")), imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")),
1.0, 1.0,
imagetext_py.TextAlign.Center, imagetext_py.TextAlign.Center,
@ -131,12 +184,19 @@ def draw_vr(saying: str):
w, h = img.size w, h = img.size
hw = 300 hw = 300
img2 = PIL.Image.new("RGBA", (w, h + hw), 'white') img2 = PIL.Image.new("RGBA", (w, h + hw), "white")
img2.paste(img, (0, hw)) img2.paste(img, (0, hw))
with imagetext_py.Writer(img2) as iw: with imagetext_py.Writer(img2) as iw:
iw.draw_text_wrapped( iw.draw_text_wrapped(
saying, w // 2, hw // 2 + 15, 0.5, 0.5, w, 64, LXGWWENKAI_REGULAR, saying,
w // 2,
hw // 2 + 15,
0.5,
0.5,
w,
64,
LXGWWENKAI_REGULAR,
imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")), imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")),
1.0, 1.0,
imagetext_py.TextAlign.Center, imagetext_py.TextAlign.Center,
@ -145,3 +205,36 @@ def draw_vr(saying: str):
return img2 return img2
@make_async
def draw_xm(saying: str, image: PIL.Image.Image | None = None):
img_base = PIL.Image.new("RGBA", xm_image.size, (255, 255, 255, 255))
with imagetext_py.Writer(img_base) as iw:
iw.draw_text_wrapped(
saying,
442,
200,
0.5,
0.5,
884,
64,
LXGWWENKAI_REGULAR,
imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")),
1.0,
imagetext_py.TextAlign.Center,
draw_emojis=True,
)
if image is not None:
image_r = image.copy().convert("RGBA")
width, height = image_r.size
base_width = img_base.size[0]
height = int(height / width * base_width)
image_r = image_r.resize((base_width, height))
# try to align center
y = 215 - image_r.height // 2
img_base.paste(image_r, (0, y), mask=image_r)
img_base.paste(xm_image, (0, 0), mask=xm_image)
return img_base

View File

@ -1,44 +0,0 @@
import nonebot
from nonebot.adapters.onebot.v11.bot import Bot
from nonebot.adapters.onebot.v11.event import GroupMessageEvent
from nonebot_plugin_alconna import UniMsg, UniMessage
from pydantic import BaseModel
class NoLuowenConfig(BaseModel):
plugin_noluowen_qqid: int = -1
plugin_noluowen_enable_group: list[int] = []
config = nonebot.get_plugin_config(NoLuowenConfig)
async def is_luowen_mentioned(evt: GroupMessageEvent, msg: UniMsg) -> bool:
if config.plugin_noluowen_qqid <= 0:
return False
if evt.user_id == config.plugin_noluowen_qqid:
return False
if evt.group_id not in config.plugin_noluowen_enable_group:
return False
txt = msg.extract_plain_text()
if "洛温" not in txt:
return False
if "罗文" in txt:
return False
if "阿特金森" in txt:
return False
return True
evt_luowen_mentioned = nonebot.on_message(rule=is_luowen_mentioned)
@evt_luowen_mentioned.handle()
async def _(evt: GroupMessageEvent, bot: Bot):
msg = (
UniMessage()
.reply(str(evt.message_id))
.at(str(config.plugin_noluowen_qqid))
.text(" 好像有人念错了你的 ID")
)
await evt_luowen_mentioned.send(await msg.export(bot=bot))

View File

@ -0,0 +1,64 @@
import random
from nonebot import on_command
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent
from nonebot.rule import to_me
from nonebot_plugin_alconna import UniMessage
from konabot.common.permsys import require_permission
async def make_sleep(event: GroupMessageEvent, bot: Bot, duration: int):
"""
让人睡着
"""
await bot.set_group_ban(
group_id=event.group_id,
user_id=event.user_id,
duration=duration,
)
seconds = duration % 60
minutes = (duration // 60) % 60
hours = duration // 3600
t1 = f"{hours} 小时 {minutes} 分钟 {seconds}"
message = f" 好好睡吧!奖励你 {t1}的睡眠💤"
message = UniMessage.at(str(event.user_id)).text(message)
await message.send(target=event, bot=bot)
cmd_sleep_night = on_command(
"此方晚安",
rule=require_permission("oyasumi") & to_me(),
aliases={"晚安"},
)
@cmd_sleep_night.handle()
async def oyasumi(event: GroupMessageEvent, bot: Bot):
"""
限定只能用 GroupMessageEvent因为它只能在 QQ 群中使用
"""
# 考虑到有人是熬夜很久,所以这里就给一个 3 到 5 小时睡眠的随机数。这个时间内
# 要睡不着我觉得是个小概率事件了!
duration = random.randint(3 * 3600, 5 * 3600)
await make_sleep(event, bot, duration)
await cmd_sleep_night.finish()
cmd_sleep_noon = on_command(
"此方午安",
rule=require_permission("oyasumi") & to_me(),
aliases={"午安"},
)
@cmd_sleep_noon.handle()
async def sleep_noon(event: GroupMessageEvent, bot: Bot):
duration = random.randint(60 * 15, 60 * 30)
await make_sleep(event, bot, duration)
await cmd_sleep_night.finish()

View File

@ -41,6 +41,7 @@ bin_path: Path | None = None
@arti_typst_linux.on_finished @arti_typst_linux.on_finished
async def _(downloaded: bool): async def _(downloaded: bool):
logger.debug("安装好了 Linux 版本的 Typst")
global bin_path global bin_path
tar_path = arti_typst_linux.target tar_path = arti_typst_linux.target
@ -71,6 +72,7 @@ async def _(downloaded: bool):
@arti_typst_windows.on_finished @arti_typst_windows.on_finished
async def _(downloaded: bool): async def _(downloaded: bool):
logger.debug("安装好了 Windows 版本的 Typst")
global bin_path global bin_path
zip_path = arti_typst_windows.target zip_path = arti_typst_windows.target
bin_path = BINARY_PATH / "typst.exe" bin_path = BINARY_PATH / "typst.exe"
@ -160,6 +162,7 @@ async def _(
# 对于本地机器,一般不会在应用启动时自动下载,这里再保证存在 # 对于本地机器,一般不会在应用启动时自动下载,这里再保证存在
await ensure_artifact(arti_typst_linux) await ensure_artifact(arti_typst_linux)
await ensure_artifact(arti_typst_windows) await ensure_artifact(arti_typst_windows)
if bin_path is None or not bin_path.exists(): if bin_path is None or not bin_path.exists():
logger.warning("当前环境不存在 Typst但仍然调用了") logger.warning("当前环境不存在 Typst但仍然调用了")
return return

1431
poetry.lock generated

File diff suppressed because it is too large Load Diff