nonebot-plugin-taozi 0.2.0__py3-none-any.whl
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.
- nonebot_plugin_taozi/__init__.py +30 -0
- nonebot_plugin_taozi/commands/__init__.py +2 -0
- nonebot_plugin_taozi/commands/common.py +71 -0
- nonebot_plugin_taozi/commands/fun.py +144 -0
- nonebot_plugin_taozi/commands/help.py +30 -0
- nonebot_plugin_taozi/commands/lexicon.py +54 -0
- nonebot_plugin_taozi/commands/settings.py +98 -0
- nonebot_plugin_taozi/config.py +17 -0
- nonebot_plugin_taozi/fortune.py +64 -0
- nonebot_plugin_taozi/interactions.py +27 -0
- nonebot_plugin_taozi/lexicon.py +116 -0
- nonebot_plugin_taozi/models.py +30 -0
- nonebot_plugin_taozi/output.py +80 -0
- nonebot_plugin_taozi/render.py +524 -0
- nonebot_plugin_taozi/resources/lexicon.json +122 -0
- nonebot_plugin_taozi/resources/taozi_character.png +0 -0
- nonebot_plugin_taozi/state.py +82 -0
- nonebot_plugin_taozi-0.2.0.dist-info/METADATA +217 -0
- nonebot_plugin_taozi-0.2.0.dist-info/RECORD +21 -0
- nonebot_plugin_taozi-0.2.0.dist-info/WHEEL +4 -0
- nonebot_plugin_taozi-0.2.0.dist-info/licenses/LICENSE +22 -0
|
@@ -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,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
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from nonebot import on_command
|
|
2
|
+
|
|
3
|
+
from ..output import finish_message_card
|
|
4
|
+
|
|
5
|
+
help_command = on_command(
|
|
6
|
+
"桃纸帮助",
|
|
7
|
+
aliases={"桃纸助手"},
|
|
8
|
+
priority=10,
|
|
9
|
+
block=True,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@help_command.handle()
|
|
14
|
+
async def handle_help() -> None:
|
|
15
|
+
body = (
|
|
16
|
+
"词典:/桃系词典 [词条]\n"
|
|
17
|
+
"随机词条:/随机桃词\n"
|
|
18
|
+
"每日互动:/今日桃签\n"
|
|
19
|
+
"自选身份:/我的桃色 [白桃/黑桃/红桃/黄桃/取消]\n"
|
|
20
|
+
"互动状态:/桃趣状态\n"
|
|
21
|
+
"群管理:/桃趣 开启|关闭|状态\n\n"
|
|
22
|
+
"词典保留出处、可信度和语境边界;互动文案不是主播原话。"
|
|
23
|
+
)
|
|
24
|
+
await finish_message_card(
|
|
25
|
+
help_command,
|
|
26
|
+
"桃纸助手",
|
|
27
|
+
body,
|
|
28
|
+
chips=("非官方", "图片模式"),
|
|
29
|
+
footer="有出处的桃系词典 · 可选的轻互动",
|
|
30
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from nonebot import on_command
|
|
2
|
+
from nonebot.adapters.onebot.v11 import Message
|
|
3
|
+
from nonebot.params import CommandArg
|
|
4
|
+
|
|
5
|
+
from ..config import plugin_config
|
|
6
|
+
from ..lexicon import BUILTIN_LEXICON, render_entry
|
|
7
|
+
from ..output import finish_lexicon_card, finish_message_card
|
|
8
|
+
|
|
9
|
+
lexicon_command = on_command(
|
|
10
|
+
"桃系词典",
|
|
11
|
+
aliases={"桃词典"},
|
|
12
|
+
priority=10,
|
|
13
|
+
block=True,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@lexicon_command.handle()
|
|
18
|
+
async def handle_lexicon(args: Message = CommandArg()) -> None:
|
|
19
|
+
query = args.extract_plain_text().strip()
|
|
20
|
+
if not query:
|
|
21
|
+
body = (
|
|
22
|
+
"已收录桃系词条:\n"
|
|
23
|
+
f"{BUILTIN_LEXICON.list_terms()}\n\n"
|
|
24
|
+
"用法:/桃系词典 黑桃"
|
|
25
|
+
)
|
|
26
|
+
await finish_message_card(
|
|
27
|
+
lexicon_command,
|
|
28
|
+
"桃系词典",
|
|
29
|
+
body,
|
|
30
|
+
chips=("有出处", "可修订"),
|
|
31
|
+
footer="输入“/桃系词典 词条名”查看解释、边界和来源。",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
entry = BUILTIN_LEXICON.find(query)
|
|
35
|
+
if entry is None:
|
|
36
|
+
suggestions = BUILTIN_LEXICON.suggest(query)
|
|
37
|
+
suffix = f"\n你可能想查:{'、'.join(suggestions)}" if suggestions else ""
|
|
38
|
+
body = (
|
|
39
|
+
f"暂未收录“{query}”。{suffix}\n"
|
|
40
|
+
"词典只收录有公开出处、能说明边界的用法。"
|
|
41
|
+
)
|
|
42
|
+
await finish_message_card(
|
|
43
|
+
lexicon_command,
|
|
44
|
+
"暂未收录",
|
|
45
|
+
body,
|
|
46
|
+
chips=("等待考证",),
|
|
47
|
+
footer="有可靠公开来源后,可以通过更新词典数据补充。",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
await finish_lexicon_card(
|
|
51
|
+
lexicon_command,
|
|
52
|
+
entry,
|
|
53
|
+
render_entry(entry, show_sources=plugin_config.taozi_lexicon_show_sources),
|
|
54
|
+
)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from nonebot import on_command
|
|
2
|
+
from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message, MessageEvent
|
|
3
|
+
from nonebot.adapters.onebot.v11.permission import GROUP_ADMIN, GROUP_OWNER
|
|
4
|
+
from nonebot.params import CommandArg
|
|
5
|
+
from nonebot.permission import SUPERUSER
|
|
6
|
+
|
|
7
|
+
from ..config import plugin_config
|
|
8
|
+
from ..output import finish_message_card
|
|
9
|
+
from .common import get_group_id, is_fun_enabled, state_store
|
|
10
|
+
|
|
11
|
+
fun_status = on_command("桃趣状态", priority=10, block=True)
|
|
12
|
+
fun_settings = on_command(
|
|
13
|
+
"桃趣",
|
|
14
|
+
permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER,
|
|
15
|
+
priority=10,
|
|
16
|
+
block=True,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@fun_status.handle()
|
|
21
|
+
async def handle_fun_status(event: MessageEvent) -> None:
|
|
22
|
+
enabled = await is_fun_enabled(event)
|
|
23
|
+
state = "开启" if enabled else "关闭"
|
|
24
|
+
detail = ""
|
|
25
|
+
if not plugin_config.taozi_fun_enabled:
|
|
26
|
+
detail = "\n全局配置 TAOZI_FUN_ENABLED=false。"
|
|
27
|
+
body = f"当前会话的桃趣互动:{state}。{detail}\n桃系词典不受此开关影响。"
|
|
28
|
+
await finish_message_card(
|
|
29
|
+
fun_status,
|
|
30
|
+
"桃趣状态",
|
|
31
|
+
body,
|
|
32
|
+
chips=(state, "词典始终可用"),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@fun_settings.handle()
|
|
37
|
+
async def handle_fun_settings(event: MessageEvent, args: Message = CommandArg()) -> None:
|
|
38
|
+
if not isinstance(event, GroupMessageEvent):
|
|
39
|
+
await finish_message_card(
|
|
40
|
+
fun_settings,
|
|
41
|
+
"无法设置",
|
|
42
|
+
"桃趣开关只能在群聊中设置。",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
action = args.extract_plain_text().strip()
|
|
46
|
+
group_id = get_group_id(event)
|
|
47
|
+
if group_id is None:
|
|
48
|
+
await finish_message_card(
|
|
49
|
+
fun_settings,
|
|
50
|
+
"无法设置",
|
|
51
|
+
"无法识别当前群聊。",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if action in {"开启", "打开", "启用"}:
|
|
55
|
+
await state_store.set_group_enabled(group_id, True)
|
|
56
|
+
if plugin_config.taozi_fun_enabled:
|
|
57
|
+
await finish_message_card(
|
|
58
|
+
fun_settings,
|
|
59
|
+
"桃趣设置",
|
|
60
|
+
"已开启本群的桃趣互动。",
|
|
61
|
+
chips=("已开启",),
|
|
62
|
+
)
|
|
63
|
+
body = (
|
|
64
|
+
"已保存本群的开启设置,但全局配置 TAOZI_FUN_ENABLED=false,"
|
|
65
|
+
"修改配置并重启后才会生效。"
|
|
66
|
+
)
|
|
67
|
+
await finish_message_card(
|
|
68
|
+
fun_settings,
|
|
69
|
+
"设置已保存",
|
|
70
|
+
body,
|
|
71
|
+
chips=("等待全局开启",),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if action in {"关闭", "禁用"}:
|
|
75
|
+
await state_store.set_group_enabled(group_id, False)
|
|
76
|
+
await finish_message_card(
|
|
77
|
+
fun_settings,
|
|
78
|
+
"桃趣设置",
|
|
79
|
+
"已关闭本群的桃趣互动;桃系词典仍可使用。",
|
|
80
|
+
chips=("已关闭", "词典仍可用"),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if action == "状态":
|
|
84
|
+
enabled = await is_fun_enabled(event)
|
|
85
|
+
state = "开启" if enabled else "关闭"
|
|
86
|
+
await finish_message_card(
|
|
87
|
+
fun_settings,
|
|
88
|
+
"桃趣状态",
|
|
89
|
+
f"本群桃趣互动当前为:{state}。",
|
|
90
|
+
chips=(state,),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
await finish_message_card(
|
|
94
|
+
fun_settings,
|
|
95
|
+
"桃趣设置",
|
|
96
|
+
"用法:/桃趣 开启|/桃趣 关闭|/桃趣 状态",
|
|
97
|
+
chips=("仅群管理可用",),
|
|
98
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from nonebot import get_plugin_config
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Config(BaseModel):
|
|
6
|
+
"""桃纸助手配置。所有配置均有默认值,插件可零配置加载。"""
|
|
7
|
+
|
|
8
|
+
taozi_fun_enabled: bool = True
|
|
9
|
+
taozi_fun_cooldown_seconds: int = Field(default=8, ge=0, le=300)
|
|
10
|
+
taozi_lexicon_show_sources: bool = True
|
|
11
|
+
taozi_timezone: str = "Asia/Shanghai"
|
|
12
|
+
taozi_image_enabled: bool = True
|
|
13
|
+
taozi_image_fallback_text: bool = True
|
|
14
|
+
taozi_font: str = ""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
plugin_config = get_plugin_config(Config)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import date
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class Fortune:
|
|
11
|
+
name: str
|
|
12
|
+
message: str
|
|
13
|
+
keyword: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
FORTUNES = (
|
|
17
|
+
Fortune("出货签", "今天适合随缘探索。看见问号,就去看看。", "探索"),
|
|
18
|
+
Fortune("老演员签", "可能会碰见熟面孔。别急,换个地方再试试。", "耐心"),
|
|
19
|
+
Fortune("无情机器签", "挑一件小事认真做完:收菜、钓鱼或解图鉴都可以。", "专注"),
|
|
20
|
+
Fortune("头部主播签", "自信一点,有想法就先做起来。", "行动"),
|
|
21
|
+
Fortune("好穷签", "资源少一点也没事,先享受手头能玩的部分。", "知足"),
|
|
22
|
+
Fortune("帽险家签", "今天适合去没走过的地方,给自己留一点惊喜。", "发现"),
|
|
23
|
+
Fortune("师父签", "遇到不确定的事,可以听听大家的建议再决定。", "交流"),
|
|
24
|
+
Fortune("六冠王签", "目标可以有,但过程也要好玩。慢慢来。", "平衡"),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def pick_daily_fortune(user_id: str, day: date, *, salt: str = "taozi") -> Fortune:
|
|
29
|
+
payload = f"{salt}:{day.isoformat()}:{user_id}".encode()
|
|
30
|
+
digest = hashlib.sha256(payload).digest()
|
|
31
|
+
index = int.from_bytes(digest[:8], byteorder="big") % len(FORTUNES)
|
|
32
|
+
return FORTUNES[index]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def render_fortune(fortune: Fortune) -> str:
|
|
36
|
+
return "\n".join(
|
|
37
|
+
[
|
|
38
|
+
f"【今日桃签·{fortune.name}】",
|
|
39
|
+
fortune.message,
|
|
40
|
+
f"今日关键词:{fortune.keyword}",
|
|
41
|
+
"——互动文案,仅供娱乐,不是主播原话。",
|
|
42
|
+
]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Cooldown:
|
|
47
|
+
def __init__(self, seconds: int) -> None:
|
|
48
|
+
self._seconds = seconds
|
|
49
|
+
self._last_seen: dict[str, float] = {}
|
|
50
|
+
|
|
51
|
+
def acquire(self, key: str, *, now: float | None = None) -> float:
|
|
52
|
+
if self._seconds <= 0:
|
|
53
|
+
return 0.0
|
|
54
|
+
|
|
55
|
+
current = time.monotonic() if now is None else now
|
|
56
|
+
previous = self._last_seen.get(key)
|
|
57
|
+
if previous is not None:
|
|
58
|
+
remaining = self._seconds - (current - previous)
|
|
59
|
+
if remaining > 0:
|
|
60
|
+
return remaining
|
|
61
|
+
|
|
62
|
+
self._last_seen[key] = current
|
|
63
|
+
return 0.0
|
|
64
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .lexicon import normalize_term
|
|
4
|
+
|
|
5
|
+
ALLOWED_SELF_COLORS = ("白桃", "黑桃", "红桃", "黄桃")
|
|
6
|
+
_COLOR_INDEX = {normalize_term(color): color for color in ALLOWED_SELF_COLORS}
|
|
7
|
+
_CANCEL_WORDS = {"取消", "移除", "清除", "无"}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parse_self_color(value: str) -> str | None:
|
|
11
|
+
return _COLOR_INDEX.get(normalize_term(value))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_cancel_request(value: str) -> bool:
|
|
15
|
+
return normalize_term(value) in _CANCEL_WORDS
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def render_color_selected(color: str) -> str:
|
|
19
|
+
lines = [
|
|
20
|
+
f"已将你自选的桃色记录为:{color}。",
|
|
21
|
+
"这是你主动选择的插件内玩笑身份,不代表机器人对你的评价。",
|
|
22
|
+
"发送“/我的桃色 取消”可随时移除。",
|
|
23
|
+
]
|
|
24
|
+
if color == "红桃":
|
|
25
|
+
lines.append("提示:红桃的稳定社区含义目前仍未核实,这里只记录名称。")
|
|
26
|
+
return "\n".join(lines)
|
|
27
|
+
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import difflib
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from importlib import resources
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from pydantic import TypeAdapter
|
|
11
|
+
|
|
12
|
+
from .models import LexiconEntry
|
|
13
|
+
|
|
14
|
+
_ENTRY_LIST_ADAPTER = TypeAdapter(list[LexiconEntry])
|
|
15
|
+
_SPACE_RE = re.compile(r"\s+")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def normalize_term(value: str) -> str:
|
|
19
|
+
"""Normalize user input without changing the displayed canonical term."""
|
|
20
|
+
|
|
21
|
+
return _SPACE_RE.sub("", value).casefold()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TaoziLexicon:
|
|
25
|
+
def __init__(self, entries: list[LexiconEntry]) -> None:
|
|
26
|
+
if not entries:
|
|
27
|
+
raise ValueError("桃系词典不能为空")
|
|
28
|
+
|
|
29
|
+
self._entries = tuple(entries)
|
|
30
|
+
self._index: dict[str, LexiconEntry] = {}
|
|
31
|
+
for entry in self._entries:
|
|
32
|
+
for name in (entry.term, *entry.aliases):
|
|
33
|
+
normalized = normalize_term(name)
|
|
34
|
+
previous = self._index.get(normalized)
|
|
35
|
+
if previous is not None and previous.term != entry.term:
|
|
36
|
+
raise ValueError(f"词条别名冲突:{name}")
|
|
37
|
+
self._index[normalized] = entry
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def from_json_text(cls, payload: str) -> TaoziLexicon:
|
|
41
|
+
raw: Any = json.loads(payload)
|
|
42
|
+
return cls(_ENTRY_LIST_ADAPTER.validate_python(raw))
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def from_path(cls, path: Path) -> TaoziLexicon:
|
|
46
|
+
return cls.from_json_text(path.read_text(encoding="utf-8"))
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def entries(self) -> tuple[LexiconEntry, ...]:
|
|
50
|
+
return self._entries
|
|
51
|
+
|
|
52
|
+
def find(self, query: str) -> LexiconEntry | None:
|
|
53
|
+
return self._index.get(normalize_term(query))
|
|
54
|
+
|
|
55
|
+
def suggest(self, query: str, *, limit: int = 3) -> list[str]:
|
|
56
|
+
normalized = normalize_term(query)
|
|
57
|
+
if not normalized:
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
contained = [
|
|
61
|
+
entry.term
|
|
62
|
+
for entry in self._entries
|
|
63
|
+
if normalized in normalize_term(entry.term)
|
|
64
|
+
or any(normalized in normalize_term(alias) for alias in entry.aliases)
|
|
65
|
+
]
|
|
66
|
+
if contained:
|
|
67
|
+
return contained[:limit]
|
|
68
|
+
|
|
69
|
+
matches = difflib.get_close_matches(normalized, self._index.keys(), n=limit, cutoff=0.45)
|
|
70
|
+
suggestions: list[str] = []
|
|
71
|
+
for match in matches:
|
|
72
|
+
term = self._index[match].term
|
|
73
|
+
if term not in suggestions:
|
|
74
|
+
suggestions.append(term)
|
|
75
|
+
return suggestions
|
|
76
|
+
|
|
77
|
+
def list_terms(self) -> str:
|
|
78
|
+
return "、".join(entry.term for entry in self._entries)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def load_builtin_lexicon() -> TaoziLexicon:
|
|
82
|
+
resource = resources.files("nonebot_plugin_taozi").joinpath("resources/lexicon.json")
|
|
83
|
+
return TaoziLexicon.from_json_text(resource.read_text(encoding="utf-8"))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def render_entry(
|
|
87
|
+
entry: LexiconEntry,
|
|
88
|
+
*,
|
|
89
|
+
show_sources: bool = True,
|
|
90
|
+
compact: bool = False,
|
|
91
|
+
) -> str:
|
|
92
|
+
lines = [
|
|
93
|
+
f"【{entry.term}】",
|
|
94
|
+
f"所指对象:{entry.subject}",
|
|
95
|
+
f"解释:{entry.meaning}",
|
|
96
|
+
f"可信度:{entry.confidence}",
|
|
97
|
+
f"边界:{entry.boundary}",
|
|
98
|
+
]
|
|
99
|
+
if entry.aliases and not compact:
|
|
100
|
+
lines.insert(1, f"别名:{'、'.join(entry.aliases)}")
|
|
101
|
+
|
|
102
|
+
if show_sources:
|
|
103
|
+
lines.append("代表出处:")
|
|
104
|
+
sources = entry.sources[:1] if compact else entry.sources
|
|
105
|
+
for index, source in enumerate(sources, start=1):
|
|
106
|
+
lines.append(f"{index}. {source.title}")
|
|
107
|
+
lines.append(source.url)
|
|
108
|
+
|
|
109
|
+
if not compact:
|
|
110
|
+
lines.append(f"最后核验:{entry.verified_at}")
|
|
111
|
+
lines.append("注:非官方粉丝整理,含义会随社区语境变化。")
|
|
112
|
+
return "\n".join(lines)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
BUILTIN_LEXICON = load_builtin_lexicon()
|
|
116
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
Confidence = Literal["高", "中", "低"]
|
|
6
|
+
SubjectType = Literal["水友立场", "主播状态", "社区造词", "含义待核实"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EvidenceSource(BaseModel):
|
|
10
|
+
title: str = Field(min_length=1)
|
|
11
|
+
url: str = Field(pattern=r"^https://")
|
|
12
|
+
note: str | None = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LexiconEntry(BaseModel):
|
|
16
|
+
term: str = Field(min_length=1)
|
|
17
|
+
aliases: list[str] = Field(default_factory=list)
|
|
18
|
+
meaning: str = Field(min_length=1)
|
|
19
|
+
subject: SubjectType
|
|
20
|
+
confidence: Confidence
|
|
21
|
+
boundary: str = Field(min_length=1)
|
|
22
|
+
verified_at: str = Field(pattern=r"^\d{4}-\d{2}-\d{2}$")
|
|
23
|
+
sources: list[EvidenceSource] = Field(min_length=1)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PersistedState(BaseModel):
|
|
27
|
+
version: Literal[1] = 1
|
|
28
|
+
disabled_groups: set[str] = Field(default_factory=set)
|
|
29
|
+
self_colors: dict[str, dict[str, str]] = Field(default_factory=dict)
|
|
30
|
+
|