nonebot-plugin-taozi 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nonebot-plugin-taozi contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -0,0 +1,217 @@
1
+ Metadata-Version: 2.4
2
+ Name: nonebot-plugin-taozi
3
+ Version: 0.2.0
4
+ Summary: 图片化的有出处桃系词典与可选轻互动 NoneBot2 插件
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Keywords: nonebot,nonebot2,onebot,bilibili,taozi
8
+ Author: TonyLiangP2010405
9
+ Requires-Python: >=3.12,<3.14
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Framework :: AsyncIO
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Requires-Dist: nonebot-adapter-onebot (>=2.4.6,<3.0.0)
18
+ Requires-Dist: nonebot-plugin-localstore (>=0.7.4,<1.0.0)
19
+ Requires-Dist: nonebot2 (>=2.5.0,<3.0.0)
20
+ Requires-Dist: pillow (>=11.0,<13.0)
21
+ Requires-Dist: pydantic (>=2.0,<3.0)
22
+ Requires-Dist: qrcode (>=8.2,<9.0)
23
+ Requires-Dist: skia-python (>=144.0.post2,<145.0)
24
+ Project-URL: Homepage, https://github.com/TonyLiangP2010405/nonebot-plugin-taozi
25
+ Project-URL: Issues, https://github.com/TonyLiangP2010405/nonebot-plugin-taozi/issues
26
+ Project-URL: Repository, https://github.com/TonyLiangP2010405/nonebot-plugin-taozi
27
+ Description-Content-Type: text/markdown
28
+
29
+ # nonebot-plugin-taozi
30
+
31
+ 图片化的有出处桃系词典与可选轻互动 NoneBot2 插件。
32
+
33
+ 本项目是**非官方粉丝插件**。词典根据公开内容整理,明确显示所指对象、可信度、
34
+ 语境边界、核验日期和代表出处;轻互动文案仅供娱乐,不冒充主播原话。正常回复会渲染
35
+ 为 PNG 卡片,词典卡片附代表出处二维码;渲染失败时默认自动回退为纯文本。
36
+
37
+ ## 功能
38
+
39
+ - 查询桃系词条,显示来源和语境边界;
40
+ - 使用 Skia 直接生成 PNG,无需安装 Chromium、Playwright 或启动浏览器;
41
+ - 卡片标题区使用基于 UID `6867955` 当前公开头像角色绘制的淡背景插画;
42
+ - 词典图片附第一条代表出处的二维码,方便从图片继续查看来源;
43
+ - 随机抽取一个有出处的桃系词条;
44
+ - 每位用户每天获得稳定一致的“今日桃签”;
45
+ - 用户主动选择或取消自己的玩笑桃色;
46
+ - 群主、群管理员或 NoneBot 超级用户可按群关闭轻互动;
47
+ - 关闭轻互动后,词典查询仍然可用;
48
+ - 状态使用 `nonebot-plugin-localstore` 持久化,不需要数据库。
49
+
50
+ 内置词条:
51
+
52
+ `黑桃`、`白桃`、`黄桃`、`红桃`、`学霸桃`、`AI 桃`、`嘟嘟桃`
53
+
54
+ ## 安装
55
+
56
+ 使用 nb-cli:
57
+
58
+ ```bash
59
+ nb plugin install nonebot-plugin-taozi
60
+ ```
61
+
62
+ 使用 pip:
63
+
64
+ ```bash
65
+ pip install nonebot-plugin-taozi
66
+ ```
67
+
68
+ 使用 Poetry:
69
+
70
+ ```bash
71
+ poetry add nonebot-plugin-taozi
72
+ ```
73
+
74
+ 从源码参与开发:
75
+
76
+ ```bash
77
+ git clone https://github.com/TonyLiangP2010405/nonebot-plugin-taozi.git
78
+ cd nonebot-plugin-taozi
79
+ poetry install
80
+ ```
81
+
82
+ 在机器人项目的 `pyproject.toml` 中加载插件:
83
+
84
+ ```toml
85
+ [tool.nonebot]
86
+ plugins = ["nonebot_plugin_taozi"]
87
+ ```
88
+
89
+ ## 运行要求
90
+
91
+ - Python `3.12` 或 `3.13`
92
+ - NoneBot2 `>=2.5.0`
93
+ - OneBot V11 适配器
94
+
95
+ 插件采用 OneBot V11,是因为群主和群管理员权限属于适配器能力。词典和互动核心逻辑
96
+ 已与命令层分离,后续可以增加其他适配器入口。
97
+
98
+ 图片由 `skia-python` 原生绘制。锁定版本在 Linux x86_64、Linux aarch64、macOS 和
99
+ Windows 上均提供 Python 3.12/3.13 预编译 wheel,云端不需要编译 Skia。
100
+
101
+ ### 云端中文字体
102
+
103
+ 插件会自动寻找常见中文字体。Linux 镜像建议安装 Noto CJK,例如 Debian/Ubuntu:
104
+
105
+ ```bash
106
+ apt-get update && apt-get install -y fonts-noto-cjk
107
+ ```
108
+
109
+ 如果镜像中的字体位置不同,可以显式配置:
110
+
111
+ ```dotenv
112
+ TAOZI_FONT=/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc
113
+ ```
114
+
115
+ 多个候选路径用半角分号分隔。字体缺失时插件仍会启动,并在日志中提示;若图片生成失败,
116
+ 默认发送原始文字回复。
117
+
118
+ ## 配置
119
+
120
+ 所有配置均为可选项,插件可以零配置加载。
121
+
122
+ | 配置项 | 默认值 | 说明 |
123
+ |---|---:|---|
124
+ | `TAOZI_FUN_ENABLED` | `true` | 全局开启轻互动;关闭后词典仍可使用 |
125
+ | `TAOZI_FUN_COOLDOWN_SECONDS` | `8` | 同一用户、同一互动指令的冷却秒数,范围 0–300 |
126
+ | `TAOZI_LEXICON_SHOW_SOURCES` | `true` | 词典和随机词条是否显示来源链接 |
127
+ | `TAOZI_TIMEZONE` | `Asia/Shanghai` | 今日桃签使用的日期时区 |
128
+ | `TAOZI_IMAGE_ENABLED` | `true` | 将正常回复渲染为 PNG 图片 |
129
+ | `TAOZI_IMAGE_FALLBACK_TEXT` | `true` | 图片生成失败时回退为纯文本 |
130
+ | `TAOZI_FONT` | 空 | 可选中文字体路径;多个路径以分号分隔 |
131
+
132
+ 配置示例见 [`.env.example`](.env.example)。
133
+
134
+ ## 指令
135
+
136
+ 是否需要命令前缀取决于机器人项目的 `COMMAND_START` 配置。下表用 `/` 演示。
137
+
138
+ | 指令 | 权限 | 范围 | 说明 |
139
+ |---|---|---|---|
140
+ | `/桃纸帮助` | 所有人 | 群聊/私聊 | 查看帮助 |
141
+ | `/桃系词典` | 所有人 | 群聊/私聊 | 列出已收录词条 |
142
+ | `/桃系词典 黑桃` | 所有人 | 群聊/私聊 | 查询解释、边界与出处 |
143
+ | `/随机桃词` | 所有人 | 群聊/私聊 | 随机展示一个有出处的词条 |
144
+ | `/今日桃签` | 所有人 | 群聊/私聊 | 获取当日稳定桃签 |
145
+ | `/我的桃色` | 所有人 | 群聊/私聊 | 查看自己的自选桃色 |
146
+ | `/我的桃色 黑桃` | 所有人 | 群聊/私聊 | 主动选择一个玩笑身份 |
147
+ | `/我的桃色 取消` | 所有人 | 群聊/私聊 | 移除自选桃色 |
148
+ | `/取消桃色` | 所有人 | 群聊/私聊 | 移除自选桃色的快捷指令 |
149
+ | `/桃趣状态` | 所有人 | 群聊/私聊 | 查看当前会话的互动开关 |
150
+ | `/桃趣 开启` | 群主/群管理/超级用户 | 群聊 | 开启本群轻互动 |
151
+ | `/桃趣 关闭` | 群主/群管理/超级用户 | 群聊 | 关闭本群轻互动 |
152
+
153
+ 词典卡片展示词条、所指对象、可信度、核验日期、解释、语境边界和代表出处;第一条来源
154
+ 会生成二维码。关闭 `TAOZI_LEXICON_SHOW_SOURCES` 后,来源文字与二维码会同时隐藏。
155
+
156
+ ### 自选桃色的边界
157
+
158
+ 插件不会分析聊天内容,也不会自动给任何群友贴“黑桃”或“黑粉”标签。桃色只能由用户
159
+ 主动选择,并且可以随时取消。`红桃`的稳定社区含义尚未核实,选择它时插件会明确提示。
160
+
161
+ ## 词典数据
162
+
163
+ 词典位于:
164
+
165
+ ```text
166
+ nonebot_plugin_taozi/resources/lexicon.json
167
+ ```
168
+
169
+ 每个词条必须提供:
170
+
171
+ - 标准名称和别名;
172
+ - 解释与所指对象;
173
+ - 高、中或低可信度;
174
+ - 不应越过的解释边界;
175
+ - 最后核验日期;
176
+ - 至少一个 HTTPS 公开来源。
177
+
178
+ 数据在插件加载时通过 Pydantic 校验。格式错误、重复别名或缺少来源会使测试失败,避免
179
+ 把未经核验的说法静默带入发布版本。
180
+
181
+ 更完整的背景研究见
182
+ [调研报告](docs/小小桃纸哟_博主调研报告_2026-07-30.md)。
183
+
184
+ ## 开发与测试
185
+
186
+ ```bash
187
+ poetry install
188
+ poetry run python -m compileall nonebot_plugin_taozi
189
+ poetry run pytest
190
+ poetry run ruff check .
191
+ poetry build
192
+ ```
193
+
194
+ GitHub Actions 会分别在 Python 3.12 和 3.13 下执行。测试覆盖 PNG 生成和动态高度、
195
+ 词典图片命令、别名与建议、每日桃签稳定性、冷却、桃色输入边界、状态持久化,以及
196
+ NoneBot 插件加载。
197
+
198
+ ## 实现参考
199
+
200
+ 图片渲染方案参考了 MIT 许可的
201
+ [nonebot-plugin-daily-attendance](https://github.com/TonyLiangP2010405/nonebot-plugin-daily-attendance):
202
+ 沿用“Skia 原生绘制 + 系统中文字体探测 + 直接发送 PNG 字节”的部署思路。本插件的卡片
203
+ 版式、动态高度、来源二维码、异步线程封装和降级逻辑均按自身需求实现。
204
+
205
+ ## 隐私与版权
206
+
207
+ - 不收集聊天内容或构建群成员画像;
208
+ - 只保存群级互动开关和用户主动选择的桃色;
209
+ - 不直接内置或搬运主播音频、原始头像,不做 AI 声音克隆或第一人称拟态回复;
210
+ - 卡片角色图是依据账号当前公开头像重新绘制的非官方装饰素材,不表示主播授权;
211
+ - 来源链接指向原始 B 站页面,不搬运完整视频;
212
+ - 公开发布或使用名称、形象素材前,仍应确认授权和平台规则。
213
+
214
+ ## License
215
+
216
+ [MIT](LICENSE)
217
+
@@ -0,0 +1,188 @@
1
+ # nonebot-plugin-taozi
2
+
3
+ 图片化的有出处桃系词典与可选轻互动 NoneBot2 插件。
4
+
5
+ 本项目是**非官方粉丝插件**。词典根据公开内容整理,明确显示所指对象、可信度、
6
+ 语境边界、核验日期和代表出处;轻互动文案仅供娱乐,不冒充主播原话。正常回复会渲染
7
+ 为 PNG 卡片,词典卡片附代表出处二维码;渲染失败时默认自动回退为纯文本。
8
+
9
+ ## 功能
10
+
11
+ - 查询桃系词条,显示来源和语境边界;
12
+ - 使用 Skia 直接生成 PNG,无需安装 Chromium、Playwright 或启动浏览器;
13
+ - 卡片标题区使用基于 UID `6867955` 当前公开头像角色绘制的淡背景插画;
14
+ - 词典图片附第一条代表出处的二维码,方便从图片继续查看来源;
15
+ - 随机抽取一个有出处的桃系词条;
16
+ - 每位用户每天获得稳定一致的“今日桃签”;
17
+ - 用户主动选择或取消自己的玩笑桃色;
18
+ - 群主、群管理员或 NoneBot 超级用户可按群关闭轻互动;
19
+ - 关闭轻互动后,词典查询仍然可用;
20
+ - 状态使用 `nonebot-plugin-localstore` 持久化,不需要数据库。
21
+
22
+ 内置词条:
23
+
24
+ `黑桃`、`白桃`、`黄桃`、`红桃`、`学霸桃`、`AI 桃`、`嘟嘟桃`
25
+
26
+ ## 安装
27
+
28
+ 使用 nb-cli:
29
+
30
+ ```bash
31
+ nb plugin install nonebot-plugin-taozi
32
+ ```
33
+
34
+ 使用 pip:
35
+
36
+ ```bash
37
+ pip install nonebot-plugin-taozi
38
+ ```
39
+
40
+ 使用 Poetry:
41
+
42
+ ```bash
43
+ poetry add nonebot-plugin-taozi
44
+ ```
45
+
46
+ 从源码参与开发:
47
+
48
+ ```bash
49
+ git clone https://github.com/TonyLiangP2010405/nonebot-plugin-taozi.git
50
+ cd nonebot-plugin-taozi
51
+ poetry install
52
+ ```
53
+
54
+ 在机器人项目的 `pyproject.toml` 中加载插件:
55
+
56
+ ```toml
57
+ [tool.nonebot]
58
+ plugins = ["nonebot_plugin_taozi"]
59
+ ```
60
+
61
+ ## 运行要求
62
+
63
+ - Python `3.12` 或 `3.13`
64
+ - NoneBot2 `>=2.5.0`
65
+ - OneBot V11 适配器
66
+
67
+ 插件采用 OneBot V11,是因为群主和群管理员权限属于适配器能力。词典和互动核心逻辑
68
+ 已与命令层分离,后续可以增加其他适配器入口。
69
+
70
+ 图片由 `skia-python` 原生绘制。锁定版本在 Linux x86_64、Linux aarch64、macOS 和
71
+ Windows 上均提供 Python 3.12/3.13 预编译 wheel,云端不需要编译 Skia。
72
+
73
+ ### 云端中文字体
74
+
75
+ 插件会自动寻找常见中文字体。Linux 镜像建议安装 Noto CJK,例如 Debian/Ubuntu:
76
+
77
+ ```bash
78
+ apt-get update && apt-get install -y fonts-noto-cjk
79
+ ```
80
+
81
+ 如果镜像中的字体位置不同,可以显式配置:
82
+
83
+ ```dotenv
84
+ TAOZI_FONT=/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc
85
+ ```
86
+
87
+ 多个候选路径用半角分号分隔。字体缺失时插件仍会启动,并在日志中提示;若图片生成失败,
88
+ 默认发送原始文字回复。
89
+
90
+ ## 配置
91
+
92
+ 所有配置均为可选项,插件可以零配置加载。
93
+
94
+ | 配置项 | 默认值 | 说明 |
95
+ |---|---:|---|
96
+ | `TAOZI_FUN_ENABLED` | `true` | 全局开启轻互动;关闭后词典仍可使用 |
97
+ | `TAOZI_FUN_COOLDOWN_SECONDS` | `8` | 同一用户、同一互动指令的冷却秒数,范围 0–300 |
98
+ | `TAOZI_LEXICON_SHOW_SOURCES` | `true` | 词典和随机词条是否显示来源链接 |
99
+ | `TAOZI_TIMEZONE` | `Asia/Shanghai` | 今日桃签使用的日期时区 |
100
+ | `TAOZI_IMAGE_ENABLED` | `true` | 将正常回复渲染为 PNG 图片 |
101
+ | `TAOZI_IMAGE_FALLBACK_TEXT` | `true` | 图片生成失败时回退为纯文本 |
102
+ | `TAOZI_FONT` | 空 | 可选中文字体路径;多个路径以分号分隔 |
103
+
104
+ 配置示例见 [`.env.example`](.env.example)。
105
+
106
+ ## 指令
107
+
108
+ 是否需要命令前缀取决于机器人项目的 `COMMAND_START` 配置。下表用 `/` 演示。
109
+
110
+ | 指令 | 权限 | 范围 | 说明 |
111
+ |---|---|---|---|
112
+ | `/桃纸帮助` | 所有人 | 群聊/私聊 | 查看帮助 |
113
+ | `/桃系词典` | 所有人 | 群聊/私聊 | 列出已收录词条 |
114
+ | `/桃系词典 黑桃` | 所有人 | 群聊/私聊 | 查询解释、边界与出处 |
115
+ | `/随机桃词` | 所有人 | 群聊/私聊 | 随机展示一个有出处的词条 |
116
+ | `/今日桃签` | 所有人 | 群聊/私聊 | 获取当日稳定桃签 |
117
+ | `/我的桃色` | 所有人 | 群聊/私聊 | 查看自己的自选桃色 |
118
+ | `/我的桃色 黑桃` | 所有人 | 群聊/私聊 | 主动选择一个玩笑身份 |
119
+ | `/我的桃色 取消` | 所有人 | 群聊/私聊 | 移除自选桃色 |
120
+ | `/取消桃色` | 所有人 | 群聊/私聊 | 移除自选桃色的快捷指令 |
121
+ | `/桃趣状态` | 所有人 | 群聊/私聊 | 查看当前会话的互动开关 |
122
+ | `/桃趣 开启` | 群主/群管理/超级用户 | 群聊 | 开启本群轻互动 |
123
+ | `/桃趣 关闭` | 群主/群管理/超级用户 | 群聊 | 关闭本群轻互动 |
124
+
125
+ 词典卡片展示词条、所指对象、可信度、核验日期、解释、语境边界和代表出处;第一条来源
126
+ 会生成二维码。关闭 `TAOZI_LEXICON_SHOW_SOURCES` 后,来源文字与二维码会同时隐藏。
127
+
128
+ ### 自选桃色的边界
129
+
130
+ 插件不会分析聊天内容,也不会自动给任何群友贴“黑桃”或“黑粉”标签。桃色只能由用户
131
+ 主动选择,并且可以随时取消。`红桃`的稳定社区含义尚未核实,选择它时插件会明确提示。
132
+
133
+ ## 词典数据
134
+
135
+ 词典位于:
136
+
137
+ ```text
138
+ nonebot_plugin_taozi/resources/lexicon.json
139
+ ```
140
+
141
+ 每个词条必须提供:
142
+
143
+ - 标准名称和别名;
144
+ - 解释与所指对象;
145
+ - 高、中或低可信度;
146
+ - 不应越过的解释边界;
147
+ - 最后核验日期;
148
+ - 至少一个 HTTPS 公开来源。
149
+
150
+ 数据在插件加载时通过 Pydantic 校验。格式错误、重复别名或缺少来源会使测试失败,避免
151
+ 把未经核验的说法静默带入发布版本。
152
+
153
+ 更完整的背景研究见
154
+ [调研报告](docs/小小桃纸哟_博主调研报告_2026-07-30.md)。
155
+
156
+ ## 开发与测试
157
+
158
+ ```bash
159
+ poetry install
160
+ poetry run python -m compileall nonebot_plugin_taozi
161
+ poetry run pytest
162
+ poetry run ruff check .
163
+ poetry build
164
+ ```
165
+
166
+ GitHub Actions 会分别在 Python 3.12 和 3.13 下执行。测试覆盖 PNG 生成和动态高度、
167
+ 词典图片命令、别名与建议、每日桃签稳定性、冷却、桃色输入边界、状态持久化,以及
168
+ NoneBot 插件加载。
169
+
170
+ ## 实现参考
171
+
172
+ 图片渲染方案参考了 MIT 许可的
173
+ [nonebot-plugin-daily-attendance](https://github.com/TonyLiangP2010405/nonebot-plugin-daily-attendance):
174
+ 沿用“Skia 原生绘制 + 系统中文字体探测 + 直接发送 PNG 字节”的部署思路。本插件的卡片
175
+ 版式、动态高度、来源二维码、异步线程封装和降级逻辑均按自身需求实现。
176
+
177
+ ## 隐私与版权
178
+
179
+ - 不收集聊天内容或构建群成员画像;
180
+ - 只保存群级互动开关和用户主动选择的桃色;
181
+ - 不直接内置或搬运主播音频、原始头像,不做 AI 声音克隆或第一人称拟态回复;
182
+ - 卡片角色图是依据账号当前公开头像重新绘制的非官方装饰素材,不表示主播授权;
183
+ - 来源链接指向原始 B 站页面,不搬运完整视频;
184
+ - 公开发布或使用名称、形象素材前,仍应确认授权和平台规则。
185
+
186
+ ## License
187
+
188
+ [MIT](LICENSE)
@@ -0,0 +1,30 @@
1
+ from nonebot import require
2
+ from nonebot.plugin import PluginMetadata
3
+
4
+ from .config import Config, plugin_config
5
+ from .render import init_font_config
6
+
7
+ require("nonebot_plugin_localstore")
8
+ init_font_config(plugin_config.taozi_font)
9
+
10
+ __plugin_meta__ = PluginMetadata(
11
+ name="桃纸助手",
12
+ description="图片化的有出处桃系词典与可选轻互动",
13
+ usage=(
14
+ "桃系词典 [词条]|随机桃词|今日桃签|我的桃色 [桃色/取消]|"
15
+ "桃趣状态|桃趣 开启/关闭"
16
+ ),
17
+ type="application",
18
+ homepage="https://github.com/TonyLiangP2010405/nonebot-plugin-taozi",
19
+ config=Config,
20
+ supported_adapters={"~onebot.v11"},
21
+ extra={
22
+ "authoritative": False,
23
+ "disclaimer": "非官方粉丝插件;词条含义以公开来源和具体语境为准。",
24
+ },
25
+ )
26
+
27
+ from .commands import fun as fun
28
+ from .commands import help as help
29
+ from .commands import lexicon as lexicon
30
+ from .commands import settings as settings
@@ -0,0 +1,2 @@
1
+ """NoneBot command matchers for 桃纸助手."""
2
+
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
5
+
6
+ import nonebot_plugin_localstore as store
7
+ from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageEvent
8
+ from nonebot.matcher import Matcher
9
+
10
+ from ..config import plugin_config
11
+ from ..fortune import Cooldown
12
+ from ..output import finish_message_card
13
+ from ..state import TaoziStateStore
14
+
15
+ state_store = TaoziStateStore(store.get_plugin_data_file("state.json"))
16
+ fun_cooldown = Cooldown(plugin_config.taozi_fun_cooldown_seconds)
17
+
18
+
19
+ def get_group_id(event: MessageEvent) -> str | None:
20
+ if isinstance(event, GroupMessageEvent):
21
+ return str(event.group_id)
22
+ return None
23
+
24
+
25
+ def get_scope_id(event: MessageEvent) -> str:
26
+ group_id = get_group_id(event)
27
+ if group_id is not None:
28
+ return f"group:{group_id}"
29
+ return f"private:{event.get_user_id()}"
30
+
31
+
32
+ def get_today():
33
+ try:
34
+ timezone = ZoneInfo(plugin_config.taozi_timezone)
35
+ except ZoneInfoNotFoundError:
36
+ timezone = ZoneInfo("Asia/Shanghai")
37
+ return datetime.now(timezone).date()
38
+
39
+
40
+ async def is_fun_enabled(event: MessageEvent) -> bool:
41
+ if not plugin_config.taozi_fun_enabled:
42
+ return False
43
+ group_id = get_group_id(event)
44
+ if group_id is None:
45
+ return True
46
+ return await state_store.is_group_enabled(group_id, default=True)
47
+
48
+
49
+ async def require_fun(
50
+ matcher: type[Matcher],
51
+ event: MessageEvent,
52
+ *,
53
+ command_key: str,
54
+ ) -> None:
55
+ if not await is_fun_enabled(event):
56
+ await finish_message_card(
57
+ matcher,
58
+ "桃趣互动已关闭",
59
+ "本会话的桃趣互动已关闭;桃系词典仍可正常使用。",
60
+ chips=("词典仍可用",),
61
+ )
62
+
63
+ cooldown_key = f"{command_key}:{get_scope_id(event)}:{event.get_user_id()}"
64
+ remaining = fun_cooldown.acquire(cooldown_key)
65
+ if remaining > 0:
66
+ await finish_message_card(
67
+ matcher,
68
+ "稍等一下",
69
+ f"桃趣冷却中,请等待约 {remaining:.0f} 秒再试。",
70
+ chips=("冷却中",),
71
+ )
@@ -0,0 +1,144 @@
1
+ import secrets
2
+
3
+ from nonebot import on_command
4
+ from nonebot.adapters.onebot.v11 import Message, MessageEvent
5
+ from nonebot.params import CommandArg
6
+
7
+ from ..config import plugin_config
8
+ from ..fortune import pick_daily_fortune, render_fortune
9
+ from ..interactions import (
10
+ ALLOWED_SELF_COLORS,
11
+ is_cancel_request,
12
+ parse_self_color,
13
+ render_color_selected,
14
+ )
15
+ from ..lexicon import BUILTIN_LEXICON, render_entry
16
+ from ..output import finish_fortune_card, finish_lexicon_card, finish_message_card
17
+ from .common import get_scope_id, get_today, require_fun, state_store
18
+
19
+ daily_fortune = on_command(
20
+ "今日桃签",
21
+ aliases={"桃签"},
22
+ priority=10,
23
+ block=True,
24
+ )
25
+ random_term = on_command(
26
+ "随机桃词",
27
+ aliases={"随机桃梗"},
28
+ priority=10,
29
+ block=True,
30
+ )
31
+ self_color = on_command(
32
+ "我的桃色",
33
+ aliases={"我是桃"},
34
+ priority=10,
35
+ block=True,
36
+ )
37
+ cancel_color = on_command("取消桃色", priority=10, block=True)
38
+
39
+
40
+ @daily_fortune.handle()
41
+ async def handle_daily_fortune(event: MessageEvent) -> None:
42
+ await require_fun(daily_fortune, event, command_key="daily_fortune")
43
+ day = get_today()
44
+ fortune = pick_daily_fortune(event.get_user_id(), day)
45
+ await finish_fortune_card(
46
+ daily_fortune,
47
+ fortune,
48
+ day.isoformat(),
49
+ render_fortune(fortune),
50
+ )
51
+
52
+
53
+ @random_term.handle()
54
+ async def handle_random_term(event: MessageEvent) -> None:
55
+ await require_fun(random_term, event, command_key="random_term")
56
+ entry = secrets.choice(BUILTIN_LEXICON.entries)
57
+ await finish_lexicon_card(
58
+ random_term,
59
+ entry,
60
+ render_entry(
61
+ entry,
62
+ show_sources=plugin_config.taozi_lexicon_show_sources,
63
+ compact=True,
64
+ ),
65
+ compact=True,
66
+ )
67
+
68
+
69
+ @self_color.handle()
70
+ async def handle_self_color(event: MessageEvent, args: Message = CommandArg()) -> None:
71
+ await require_fun(self_color, event, command_key="self_color")
72
+ value = args.extract_plain_text().strip()
73
+ scope_id = get_scope_id(event)
74
+ user_id = event.get_user_id()
75
+
76
+ if not value:
77
+ selected = await state_store.get_self_color(scope_id, user_id)
78
+ if selected is None:
79
+ body = (
80
+ "你还没有自选桃色。\n"
81
+ f"可选:{'、'.join(ALLOWED_SELF_COLORS)}\n"
82
+ "用法:/我的桃色 黑桃"
83
+ )
84
+ await finish_message_card(
85
+ self_color,
86
+ "我的桃色",
87
+ body,
88
+ chips=("自愿选择", "随时取消"),
89
+ )
90
+ body = (
91
+ f"你当前自选的桃色是:{selected}。\n"
92
+ "这是插件内的自愿玩笑身份,不代表机器人评价。"
93
+ )
94
+ await finish_message_card(
95
+ self_color,
96
+ "我的桃色",
97
+ body,
98
+ chips=(selected or "未选择", "自愿选择"),
99
+ )
100
+
101
+ if is_cancel_request(value):
102
+ removed = await state_store.remove_self_color(scope_id, user_id)
103
+ message = "已取消你的自选桃色。" if removed else "你目前没有自选桃色。"
104
+ await finish_message_card(
105
+ self_color,
106
+ "桃色设置",
107
+ message,
108
+ chips=("已更新",),
109
+ )
110
+
111
+ color = parse_self_color(value)
112
+ if color is None:
113
+ body = (
114
+ f"不支持“{value}”。可选桃色:{'、'.join(ALLOWED_SELF_COLORS)};"
115
+ "也可以发送“/我的桃色 取消”。"
116
+ )
117
+ await finish_message_card(
118
+ self_color,
119
+ "无法识别桃色",
120
+ body,
121
+ chips=("请重新选择",),
122
+ )
123
+
124
+ await state_store.set_self_color(scope_id, user_id, color)
125
+ await finish_message_card(
126
+ self_color,
127
+ "桃色设置成功",
128
+ render_color_selected(color),
129
+ chips=(color, "自愿选择"),
130
+ footer="这只是插件内的玩笑身份;发送“/我的桃色 取消”即可移除。",
131
+ )
132
+
133
+
134
+ @cancel_color.handle()
135
+ async def handle_cancel_color(event: MessageEvent) -> None:
136
+ await require_fun(cancel_color, event, command_key="cancel_color")
137
+ removed = await state_store.remove_self_color(get_scope_id(event), event.get_user_id())
138
+ message = "已取消你的自选桃色。" if removed else "你目前没有自选桃色。"
139
+ await finish_message_card(
140
+ cancel_color,
141
+ "桃色设置",
142
+ message,
143
+ chips=("已更新",),
144
+ )