Skip to content

Commit 5a47bee

Browse files
committed
✨ add repeater plugin
1 parent bfc4e55 commit 5a47bee

8 files changed

Lines changed: 532 additions & 0 deletions

File tree

entari.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@ plugins:
7878
redirect_uri: ${{ env.WAKA_REDIRECT_URI }}
7979
background_source: Lolicon
8080

81+
.plugins.repeater:
82+
on_repeat:
83+
- min_times: 3
84+
probability: 0.3
85+
86+
- min_times: 4
87+
probability: 0.3
88+
reply: 打断复读!
89+
90+
8191
.providers.llm:
8292
api_key: ${{ env.OPENAI_API_KEY }}
8393
base_url: https://api.openai.com/v1

miraita/listeners/message_decorator.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from arclet.entari import Plugin, Session, MessageCreatedEvent
22

3+
from miraita.utils.no_reply import NoReply
4+
35
plugin = Plugin.current()
46

57

@@ -8,6 +10,9 @@ async def send_hook(session: Session[MessageCreatedEvent] | None = None) -> None
810
if session is None:
911
return
1012

13+
if session.elements.has(NoReply):
14+
return
15+
1116
_, reply = session._resolve(False, True)
1217

1318
if reply:

miraita/plugins/repeater/README.md

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# miraita-plugin-repeater
2+
3+
miraita-plugin-repeater 用于检测群聊中的复读行为,并按配置复读、打断复读或响应打断复读的人。
4+
5+
插件只在群聊中生效。机器人自己发出的消息会被记录到复读状态中,因此默认不会对同一段内容无限复读。
6+
7+
## 配置项
8+
9+
### on_repeat
10+
11+
- 类型:`RepeatAction | RepeatAction[] | null`
12+
- 默认值:`{ min_times: 2, probability: 1 }`
13+
14+
当检测到有人复读上一条消息时触发。默认行为是在同一句话连续出现 2 次后,由机器人复读一次。
15+
16+
### on_interrupt
17+
18+
- 类型:`RepeatAction | RepeatAction[] | null`
19+
- 默认值:`null`
20+
21+
当检测到复读被其他消息打断时触发。默认不响应打断复读。
22+
23+
## RepeatAction
24+
25+
### min_times
26+
27+
- 类型:`int`
28+
- 默认值:`2`
29+
30+
触发规则需要达到的最少复读次数。
31+
32+
### probability
33+
34+
- 类型:`float`
35+
- 默认值:`1`
36+
37+
触发概率,取值范围为 `0``1`
38+
39+
### content
40+
41+
- 类型:`string | null`
42+
- 默认值:`null`
43+
44+
仅当当前复读内容与该值完全一致时触发。
45+
46+
### user_times
47+
48+
- 类型:`int`
49+
- 默认值:`0`
50+
51+
当前用户参与同一句复读达到该次数时才触发。设置为 `0` 表示不限制。
52+
53+
### repeated
54+
55+
- 类型:`bool | null`
56+
- 默认值:`null`
57+
58+
限制机器人是否已经复读过当前内容。设置为 `null` 表示不限制。
59+
60+
### reply
61+
62+
- 类型:`string | null`
63+
- 默认值:`null`
64+
65+
触发规则后发送的回复模板。`on_repeat` 未设置 `reply` 时默认回复复读内容,`on_interrupt` 未设置 `reply` 时不会发送消息。
66+
67+
可用模板变量:
68+
69+
- `{content}` 当前复读内容
70+
- `{times}` 当前复读次数
71+
- `{user_id}` 当前用户 ID
72+
- `{user_name}` 当前用户名称
73+
- `{at_user}` @ 当前用户
74+
- `{self_id}` 当前机器人账号 ID
75+
- `{channel_id}` 当前频道 ID
76+
- `{guild_id}` 当前群组 ID
77+
78+
## 配置示例
79+
80+
### 概率复读
81+
82+
```yaml
83+
plugins:
84+
miraita.plugins.repeater:
85+
on_repeat:
86+
min_times: 3
87+
probability: 0.5
88+
```
89+
90+
当同一句话达到 3 次后,每次继续复读都有 50% 概率触发机器人复读。
91+
92+
### 自动打断指定复读
93+
94+
```yaml
95+
plugins:
96+
miraita.plugins.repeater:
97+
on_repeat:
98+
min_times: 2
99+
content: 这机器人又开始复读了
100+
reply: 打断复读!
101+
```
102+
103+
### 检测重复复读
104+
105+
```yaml
106+
plugins:
107+
miraita.plugins.repeater:
108+
on_repeat:
109+
min_times: 2
110+
user_times: 2
111+
reply: "{at_user}不许重复复读!"
112+
```
113+
114+
当同一个用户对同一句话复读 2 次时提醒对方。
115+
116+
### 检测打断复读
117+
118+
```yaml
119+
plugins:
120+
miraita.plugins.repeater:
121+
on_repeat:
122+
min_times: 2
123+
on_interrupt:
124+
repeated: true
125+
min_times: 3
126+
probability: 0.5
127+
reply: "{at_user}在?为什么打断复读?"
128+
```
129+
130+
当某条消息已经被机器人复读过,且复读次数达到 3 次后,有人打断复读时以 50% 概率出警。
131+
132+
### 多规则
133+
134+
```yaml
135+
plugins:
136+
miraita:
137+
.plugins.repeater:
138+
on_repeat:
139+
- min_times: 3
140+
probability: 0.5
141+
142+
- min_times: 2
143+
content: 这机器人又开始复读了
144+
reply: 打断复读!
145+
146+
- min_times: 2
147+
user_times: 2
148+
reply: "{at_user}不许重复复读!"
149+
150+
on_interrupt:
151+
- repeated: true
152+
min_times: 3
153+
probability: 0.5
154+
reply: "{at_user}在?为什么打断复读?"
155+
```
156+
157+
规则会按配置顺序依次检查,命中第一条有回复的规则后停止。
158+
159+
## 自定义回调
160+
161+
如果配置规则不够用,可以在 Python 中注册回调。回调会收到当前复读状态 `state` 和会话 `session`,返回字符串或 `MessageChain` 时发送消息。
162+
163+
```python
164+
from arclet.entari import At, MessageChain
165+
166+
from miraita.plugins.repeater import on_interrupt, on_repeat
167+
168+
169+
@on_repeat
170+
def _(state, session):
171+
if state.times >= 2 and state.content == "这机器人又开始复读了":
172+
return "打断复读!"
173+
174+
175+
@on_interrupt
176+
def _(state, session):
177+
if state.repeated and state.times >= 3:
178+
return MessageChain([At(session.user.id, name=session.user.name), "在?为什么打断复读?"])
179+
```
180+
181+
## 相关
182+
183+
- [`koishi-plugin-repeater`](https://common.koishi.chat/zh-CN/plugins/repeater.html) Koishi 复读机
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
from arclet.entari import (
2+
ChannelType,
3+
MessageCreatedEvent,
4+
MessageChain,
5+
Session,
6+
metadata,
7+
)
8+
from arclet.entari.event.send import SendResponse
9+
from arclet.letoderea import on
10+
11+
from ._types import RepeatState, StateCallback
12+
from .config import Config, config
13+
from .utils import (
14+
check_actions,
15+
check_callbacks,
16+
normalize_message,
17+
state_key,
18+
update_repeated_state,
19+
)
20+
21+
from miraita.utils.no_reply import NoReply
22+
23+
metadata(
24+
name="复读机",
25+
author=[{"name": "Komorebi", "email": "mute231010@gmail.com"}],
26+
description="检测群聊复读并概率跟随复读",
27+
classifier=["娱乐"],
28+
config=Config,
29+
)
30+
31+
32+
_states: dict[str, RepeatState] = {}
33+
_on_repeat_callbacks: list[StateCallback] = []
34+
_on_interrupt_callbacks: list[StateCallback] = []
35+
36+
37+
def on_repeat(callback: StateCallback) -> StateCallback:
38+
"""注册自定义复读回调"""
39+
_on_repeat_callbacks.append(callback)
40+
return callback
41+
42+
43+
def on_interrupt(callback: StateCallback) -> StateCallback:
44+
"""注册自定义打断回调"""
45+
_on_interrupt_callbacks.append(callback)
46+
return callback
47+
48+
49+
def _get_state(key: str) -> RepeatState:
50+
return _states.setdefault(key, RepeatState())
51+
52+
53+
@on(SendResponse)
54+
async def record_bot_message(event: SendResponse):
55+
if event.session is None:
56+
return
57+
if not event.result:
58+
return
59+
60+
session = event.session
61+
if not isinstance(session.event, MessageCreatedEvent):
62+
return
63+
if not session.event.guild:
64+
return
65+
66+
content = normalize_message(event.message)
67+
if not content:
68+
return
69+
70+
key = state_key(
71+
event.account.platform,
72+
event.account.self_id,
73+
event.channel,
74+
session.event.guild.id,
75+
)
76+
update_repeated_state(_get_state(key), content)
77+
78+
79+
@on(MessageCreatedEvent, priority=20)
80+
async def handle_message(session: Session[MessageCreatedEvent]):
81+
if session.channel.type == ChannelType.DIRECT:
82+
return
83+
if not session.event.guild:
84+
return
85+
if session.user.id == session.account.self_id:
86+
return
87+
88+
content = normalize_message(session.elements)
89+
if not content:
90+
return
91+
92+
key = state_key(
93+
session.account.platform,
94+
session.account.self_id,
95+
session.channel.id,
96+
session.event.guild.id,
97+
)
98+
state = _get_state(key)
99+
100+
if content == state.content:
101+
state.times += 1
102+
state.users[session.user.id] = state.users.get(session.user.id, 0) + 1
103+
104+
reply = await check_callbacks(_on_repeat_callbacks, state, session)
105+
if not reply:
106+
reply = check_actions(
107+
config.on_repeat,
108+
state,
109+
session,
110+
"{content}",
111+
default_repeated=False,
112+
)
113+
if reply:
114+
await session.send(MessageChain(reply) + NoReply())
115+
return
116+
117+
reply = await check_callbacks(_on_interrupt_callbacks, state, session)
118+
if not reply:
119+
reply = check_actions(config.on_interrupt, state, session, None)
120+
if reply:
121+
await session.send(MessageChain(reply) + NoReply())
122+
return
123+
124+
state.content = content
125+
state.repeated = False
126+
state.times = 1
127+
state.users = {session.user.id: 1}

miraita/plugins/repeater/_types.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from collections.abc import Awaitable, Callable, Mapping, Sequence
2+
from dataclasses import dataclass, field
3+
from typing import Any, TypeAlias
4+
5+
from arclet.entari import MessageChain, MessageCreatedEvent, Session
6+
7+
from .config import RepeatAction
8+
9+
10+
@dataclass
11+
class RepeatState:
12+
content: str = ""
13+
repeated: bool = False
14+
times: int = 0
15+
users: dict[str, int] = field(default_factory=dict)
16+
17+
18+
StateResult: TypeAlias = str | MessageChain | None
19+
StateCallback: TypeAlias = Callable[
20+
[RepeatState, Session[MessageCreatedEvent]], StateResult | Awaitable[StateResult]
21+
]
22+
ActionConfig: TypeAlias = (
23+
RepeatAction | Mapping[str, Any] | Sequence[RepeatAction | Mapping[str, Any]] | None
24+
)

0 commit comments

Comments
 (0)