nonebot-plugin-group-chat-memory 0.1.3__tar.gz → 0.1.4__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.
- {nonebot_plugin_group_chat_memory-0.1.3 → nonebot_plugin_group_chat_memory-0.1.4}/PKG-INFO +8 -3
- {nonebot_plugin_group_chat_memory-0.1.3 → nonebot_plugin_group_chat_memory-0.1.4}/README.md +7 -2
- {nonebot_plugin_group_chat_memory-0.1.3 → nonebot_plugin_group_chat_memory-0.1.4}/nonebot_plugin_group_chat_memory/config.py +1 -0
- {nonebot_plugin_group_chat_memory-0.1.3 → nonebot_plugin_group_chat_memory-0.1.4}/nonebot_plugin_group_chat_memory/handlers.py +41 -9
- {nonebot_plugin_group_chat_memory-0.1.3 → nonebot_plugin_group_chat_memory-0.1.4}/nonebot_plugin_group_chat_memory/store.py +118 -5
- {nonebot_plugin_group_chat_memory-0.1.3 → nonebot_plugin_group_chat_memory-0.1.4}/pyproject.toml +1 -1
- {nonebot_plugin_group_chat_memory-0.1.3 → nonebot_plugin_group_chat_memory-0.1.4}/LICENSE +0 -0
- {nonebot_plugin_group_chat_memory-0.1.3 → nonebot_plugin_group_chat_memory-0.1.4}/nonebot_plugin_group_chat_memory/__init__.py +0 -0
- {nonebot_plugin_group_chat_memory-0.1.3 → nonebot_plugin_group_chat_memory-0.1.4}/nonebot_plugin_group_chat_memory/api.py +0 -0
- {nonebot_plugin_group_chat_memory-0.1.3 → nonebot_plugin_group_chat_memory-0.1.4}/nonebot_plugin_group_chat_memory/context.py +0 -0
- {nonebot_plugin_group_chat_memory-0.1.3 → nonebot_plugin_group_chat_memory-0.1.4}/nonebot_plugin_group_chat_memory/memory.py +0 -0
- {nonebot_plugin_group_chat_memory-0.1.3 → nonebot_plugin_group_chat_memory-0.1.4}/nonebot_plugin_group_chat_memory/models.py +0 -0
- {nonebot_plugin_group_chat_memory-0.1.3 → nonebot_plugin_group_chat_memory-0.1.4}/nonebot_plugin_group_chat_memory/replies.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nonebot-plugin-group-chat-memory
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: 为 NoneBot2 群聊提供共享长期记忆、点名对话和按群概率回复
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -105,6 +105,7 @@ COMMAND_START=["/"]
|
|
|
105
105
|
| `GROUP_CHAT_MEMORY_LONG_TERM_MEMORIES` | `5` | 每次回复最多检索的长期片段数;设为 0 可停用长期检索。 |
|
|
106
106
|
| `GROUP_CHAT_MEMORY_CONTEXT_MAX_CHARS` | `6000` | 发送给聊天模型的上下文总字符预算,至少为 1;优先保留当前消息和最近消息。 |
|
|
107
107
|
| `GROUP_CHAT_MEMORY_DEFAULT_PROBABILITY` | `0` | 新群随机回复概率,范围 0~100;已有群使用数据库中的概率。 |
|
|
108
|
+
| `GROUP_CHAT_MEMORY_HOURLY_REQUEST_LIMIT` | `10` | 新群默认每小时模型请求上限,范围 1~10000;已有群可用 `/每小时次数` 修改。 |
|
|
108
109
|
| `GROUP_CHAT_MEMORY_IGNORED_QQ` | `[]` | 完全忽略的 QQ 号 JSON 数组,例如 `[12345, 67890]`;这些账号的消息不记忆也不触发回复。 |
|
|
109
110
|
| `GROUP_CHAT_MEMORY_SYSTEM_PROMPT` | 内置“馒头 + 五更琉璃(黑猫)式性格”人格 | 自定义系统提示词并完全覆盖默认人格;设为空时仅使用内置的发言者区分和当前消息约束。 |
|
|
110
111
|
|
|
@@ -114,7 +115,9 @@ COMMAND_START=["/"]
|
|
|
114
115
|
|
|
115
116
|
在群里 @Bot 或回复 Bot 的消息即可对话;这两种定向消息必定进入回复队列,不受随机概率影响。普通纯文本按当前群概率触发,默认 0% 表示关闭随机回复,100% 表示每条符合条件的普通文本都回复。
|
|
116
117
|
|
|
117
|
-
默认人格为“馒头”:Bot 始终以馒头自称,借用五更琉璃(黑猫)的中二、毒舌、傲娇和外冷内热性格,但不会冒充黑猫本人。插件命令优先级为 `-
|
|
118
|
+
默认人格为“馒头”:Bot 始终以馒头自称,借用五更琉璃(黑猫)的中二、毒舌、傲娇和外冷内热性格,但不会冒充黑猫本人。插件命令优先级为 `-1001`,群聊消息优先级为 `-1000`;@Bot 或回复 Bot 处理完成后会停止事件继续传播,避免其他 NoneBot 插件同时响应。普通群聊仍不会阻塞其他插件。
|
|
119
|
+
|
|
120
|
+
每个群默认每小时最多触发 10 次模型请求,采用滚动 60 分钟窗口。点名超额会收到简短提示,随机回复超额静默跳过;消息仍写入记忆,命令和 Embedding 不消耗次数。
|
|
118
121
|
|
|
119
122
|
| 命令 | 权限 | 行为 |
|
|
120
123
|
|---|---|---|
|
|
@@ -125,7 +128,9 @@ COMMAND_START=["/"]
|
|
|
125
128
|
| `/模型配置` | SUPERUSER,私聊或群聊 | 查询全局配置状态,密钥只显示掩码。 |
|
|
126
129
|
| `/回复概率` | 仅 SUPERUSER | 查询当前群概率,显示一位小数。 |
|
|
127
130
|
| `/回复概率 <0~100>` | 仅 SUPERUSER | 设置当前群概率,最多一位小数,例如 `/回复概率 5.5`。 |
|
|
128
|
-
|
|
|
131
|
+
| `/每小时次数` | 仅 SUPERUSER | 查询当前群每小时模型请求上限。 |
|
|
132
|
+
| `/每小时次数 <1~10000>` | 仅 SUPERUSER | 设置当前群每小时请求上限,例如 `/每小时次数 20`。 |
|
|
133
|
+
| `/清空群记忆` | 仅 SUPERUSER | 删除当前群原始消息、分块、向量和请求计数;保留概率与每小时上限,不影响其他群。 |
|
|
129
134
|
|
|
130
135
|
非 SUPERUSER 会收到简短权限提示。命令不进入记忆,也不触发随机回复。SUPERUSER 通过 NoneBot 的 `SUPERUSERS` 配置,而不是群管理员身份确定。
|
|
131
136
|
|
|
@@ -79,6 +79,7 @@ COMMAND_START=["/"]
|
|
|
79
79
|
| `GROUP_CHAT_MEMORY_LONG_TERM_MEMORIES` | `5` | 每次回复最多检索的长期片段数;设为 0 可停用长期检索。 |
|
|
80
80
|
| `GROUP_CHAT_MEMORY_CONTEXT_MAX_CHARS` | `6000` | 发送给聊天模型的上下文总字符预算,至少为 1;优先保留当前消息和最近消息。 |
|
|
81
81
|
| `GROUP_CHAT_MEMORY_DEFAULT_PROBABILITY` | `0` | 新群随机回复概率,范围 0~100;已有群使用数据库中的概率。 |
|
|
82
|
+
| `GROUP_CHAT_MEMORY_HOURLY_REQUEST_LIMIT` | `10` | 新群默认每小时模型请求上限,范围 1~10000;已有群可用 `/每小时次数` 修改。 |
|
|
82
83
|
| `GROUP_CHAT_MEMORY_IGNORED_QQ` | `[]` | 完全忽略的 QQ 号 JSON 数组,例如 `[12345, 67890]`;这些账号的消息不记忆也不触发回复。 |
|
|
83
84
|
| `GROUP_CHAT_MEMORY_SYSTEM_PROMPT` | 内置“馒头 + 五更琉璃(黑猫)式性格”人格 | 自定义系统提示词并完全覆盖默认人格;设为空时仅使用内置的发言者区分和当前消息约束。 |
|
|
84
85
|
|
|
@@ -88,7 +89,9 @@ COMMAND_START=["/"]
|
|
|
88
89
|
|
|
89
90
|
在群里 @Bot 或回复 Bot 的消息即可对话;这两种定向消息必定进入回复队列,不受随机概率影响。普通纯文本按当前群概率触发,默认 0% 表示关闭随机回复,100% 表示每条符合条件的普通文本都回复。
|
|
90
91
|
|
|
91
|
-
默认人格为“馒头”:Bot 始终以馒头自称,借用五更琉璃(黑猫)的中二、毒舌、傲娇和外冷内热性格,但不会冒充黑猫本人。插件命令优先级为 `-
|
|
92
|
+
默认人格为“馒头”:Bot 始终以馒头自称,借用五更琉璃(黑猫)的中二、毒舌、傲娇和外冷内热性格,但不会冒充黑猫本人。插件命令优先级为 `-1001`,群聊消息优先级为 `-1000`;@Bot 或回复 Bot 处理完成后会停止事件继续传播,避免其他 NoneBot 插件同时响应。普通群聊仍不会阻塞其他插件。
|
|
93
|
+
|
|
94
|
+
每个群默认每小时最多触发 10 次模型请求,采用滚动 60 分钟窗口。点名超额会收到简短提示,随机回复超额静默跳过;消息仍写入记忆,命令和 Embedding 不消耗次数。
|
|
92
95
|
|
|
93
96
|
| 命令 | 权限 | 行为 |
|
|
94
97
|
|---|---|---|
|
|
@@ -99,7 +102,9 @@ COMMAND_START=["/"]
|
|
|
99
102
|
| `/模型配置` | SUPERUSER,私聊或群聊 | 查询全局配置状态,密钥只显示掩码。 |
|
|
100
103
|
| `/回复概率` | 仅 SUPERUSER | 查询当前群概率,显示一位小数。 |
|
|
101
104
|
| `/回复概率 <0~100>` | 仅 SUPERUSER | 设置当前群概率,最多一位小数,例如 `/回复概率 5.5`。 |
|
|
102
|
-
|
|
|
105
|
+
| `/每小时次数` | 仅 SUPERUSER | 查询当前群每小时模型请求上限。 |
|
|
106
|
+
| `/每小时次数 <1~10000>` | 仅 SUPERUSER | 设置当前群每小时请求上限,例如 `/每小时次数 20`。 |
|
|
107
|
+
| `/清空群记忆` | 仅 SUPERUSER | 删除当前群原始消息、分块、向量和请求计数;保留概率与每小时上限,不影响其他群。 |
|
|
103
108
|
|
|
104
109
|
非 SUPERUSER 会收到简短权限提示。命令不进入记忆,也不触发随机回复。SUPERUSER 通过 NoneBot 的 `SUPERUSERS` 配置,而不是群管理员身份确定。
|
|
105
110
|
|
|
@@ -67,6 +67,7 @@ class Config(BaseModel):
|
|
|
67
67
|
group_chat_memory_long_term_memories: int = Field(default=5, ge=0)
|
|
68
68
|
group_chat_memory_context_max_chars: int = Field(default=6000, ge=1)
|
|
69
69
|
group_chat_memory_default_probability: float = Field(default=0.0, ge=0.0, le=100.0)
|
|
70
|
+
group_chat_memory_hourly_request_limit: int = Field(default=10, ge=1, le=10000)
|
|
70
71
|
group_chat_memory_ignored_qq: set[int] = Field(default_factory=set)
|
|
71
72
|
group_chat_memory_system_prompt: str = DEFAULT_SYSTEM_PROMPT
|
|
72
73
|
|
|
@@ -35,6 +35,8 @@ logger = logging.getLogger(__name__)
|
|
|
35
35
|
GENERIC_REPLY_ERROR = "暂时无法生成回复,请稍后再试。"
|
|
36
36
|
PERMISSION_DENIED = "仅 SUPERUSER 可执行此命令。"
|
|
37
37
|
INVALID_PROBABILITY = "概率需为 0~100 的数字,最多一位小数。"
|
|
38
|
+
INVALID_HOURLY_LIMIT = "每小时请求上限必须是 1 到 10000 的整数。"
|
|
39
|
+
RATE_LIMIT_NOTICE = "本群本小时请求次数已用完,请稍后再来。"
|
|
38
40
|
PROBABILITY_PATTERN = re.compile(r"(?:100(?:\.0)?|\d{1,2}(?:\.\d)?)")
|
|
39
41
|
SECRET_COMMAND_LOG_PATTERN = re.compile(r"(设置聊天密钥|设置向量密钥)\s+[^\r\n]*")
|
|
40
42
|
|
|
@@ -138,11 +140,11 @@ async def apply_setting(key: str, value: str) -> None:
|
|
|
138
140
|
config.apply_runtime_config(values)
|
|
139
141
|
|
|
140
142
|
|
|
141
|
-
chat_url_matcher = on_command("设置聊天地址", priority=-
|
|
142
|
-
chat_key_matcher = on_command("设置聊天密钥", priority=-
|
|
143
|
-
chat_model_matcher = on_command("设置聊天模型", priority=-
|
|
144
|
-
embedding_key_matcher = on_command("设置向量密钥", priority=-
|
|
145
|
-
model_config_matcher = on_command("模型配置", priority=-
|
|
143
|
+
chat_url_matcher = on_command("设置聊天地址", priority=-1001, block=True)
|
|
144
|
+
chat_key_matcher = on_command("设置聊天密钥", priority=-1001, block=True)
|
|
145
|
+
chat_model_matcher = on_command("设置聊天模型", priority=-1001, block=True)
|
|
146
|
+
embedding_key_matcher = on_command("设置向量密钥", priority=-1001, block=True)
|
|
147
|
+
model_config_matcher = on_command("模型配置", priority=-1001, block=True)
|
|
146
148
|
|
|
147
149
|
|
|
148
150
|
def _valid_http_url(value: str) -> bool:
|
|
@@ -226,10 +228,15 @@ async def handle_model_config(bot: Bot, event: MessageEvent) -> None:
|
|
|
226
228
|
|
|
227
229
|
|
|
228
230
|
probability_matcher = on_command(
|
|
229
|
-
"回复概率", rule=_eligible_group_message, priority=-
|
|
231
|
+
"回复概率", rule=_eligible_group_message, priority=-1001, block=True
|
|
230
232
|
)
|
|
231
|
-
clear_matcher = on_command(
|
|
232
|
-
|
|
233
|
+
clear_matcher = on_command(
|
|
234
|
+
"清空群记忆", rule=_eligible_group_message, priority=-1001, block=True
|
|
235
|
+
)
|
|
236
|
+
hourly_limit_matcher = on_command(
|
|
237
|
+
"每小时次数", rule=_eligible_group_message, priority=-1001, block=True
|
|
238
|
+
)
|
|
239
|
+
group_message_matcher = on_message(rule=_eligible_group_message, priority=-1000, block=False)
|
|
233
240
|
|
|
234
241
|
|
|
235
242
|
@probability_matcher.handle()
|
|
@@ -261,6 +268,27 @@ async def handle_clear(bot: Bot, event: GroupMessageEvent) -> None:
|
|
|
261
268
|
await clear_matcher.finish("已清空当前群记忆。")
|
|
262
269
|
|
|
263
270
|
|
|
271
|
+
@hourly_limit_matcher.handle()
|
|
272
|
+
async def handle_hourly_limit(
|
|
273
|
+
bot: Bot,
|
|
274
|
+
event: GroupMessageEvent,
|
|
275
|
+
args: Annotated[Message, CommandArg()],
|
|
276
|
+
) -> None:
|
|
277
|
+
if not await SUPERUSER(bot, event):
|
|
278
|
+
await hourly_limit_matcher.finish(PERMISSION_DENIED)
|
|
279
|
+
|
|
280
|
+
text = args.extract_plain_text().strip()
|
|
281
|
+
if not text:
|
|
282
|
+
limit = await store.get_hourly_limit(event.group_id)
|
|
283
|
+
await hourly_limit_matcher.finish(f"当前群每小时请求上限:{limit} 次")
|
|
284
|
+
if not text.isdigit() or not 1 <= int(text) <= 10000:
|
|
285
|
+
await hourly_limit_matcher.finish(INVALID_HOURLY_LIMIT)
|
|
286
|
+
|
|
287
|
+
limit = int(text)
|
|
288
|
+
await store.set_hourly_limit(event.group_id, limit)
|
|
289
|
+
await hourly_limit_matcher.finish(f"已设置当前群每小时请求上限:{limit} 次")
|
|
290
|
+
|
|
291
|
+
|
|
264
292
|
@group_message_matcher.handle()
|
|
265
293
|
async def handle_group_message(bot: Bot, event: GroupMessageEvent) -> None:
|
|
266
294
|
explicit_at = any(
|
|
@@ -288,12 +316,16 @@ async def handle_group_message(bot: Bot, event: GroupMessageEvent) -> None:
|
|
|
288
316
|
_bots[event.group_id] = bot
|
|
289
317
|
await writer.record(memory_message)
|
|
290
318
|
if directed:
|
|
291
|
-
|
|
319
|
+
if await store.consume_request_slot(event.group_id):
|
|
320
|
+
coordinator.submit(event.group_id, memory_message, explicit=True)
|
|
321
|
+
else:
|
|
322
|
+
await bot.send_group_msg(group_id=event.group_id, message=RATE_LIMIT_NOTICE)
|
|
292
323
|
raise StopPropagation
|
|
293
324
|
elif (
|
|
294
325
|
event.reply is None
|
|
295
326
|
and all(segment.type == "text" for segment in event.message)
|
|
296
327
|
and await coordinator.should_random_reply(event.group_id)
|
|
328
|
+
and await store.consume_request_slot(event.group_id)
|
|
297
329
|
):
|
|
298
330
|
coordinator.submit(event.group_id, memory_message, explicit=False)
|
|
299
331
|
|
|
@@ -7,7 +7,7 @@ import sqlite3
|
|
|
7
7
|
import struct
|
|
8
8
|
from collections.abc import AsyncIterator, Sequence
|
|
9
9
|
from contextlib import asynccontextmanager, suppress
|
|
10
|
-
from datetime import datetime, timezone
|
|
10
|
+
from datetime import datetime, timedelta, timezone
|
|
11
11
|
|
|
12
12
|
import aiosqlite
|
|
13
13
|
import sqlite_vec
|
|
@@ -41,10 +41,23 @@ _SCHEMA_STATEMENTS = (
|
|
|
41
41
|
group_id INTEGER PRIMARY KEY,
|
|
42
42
|
probability REAL NOT NULL DEFAULT 0
|
|
43
43
|
CHECK (probability >= 0 AND probability <= 100),
|
|
44
|
+
hourly_limit INTEGER NOT NULL DEFAULT 10
|
|
45
|
+
CHECK (hourly_limit >= 1 AND hourly_limit <= 10000),
|
|
44
46
|
updated_at TEXT NOT NULL
|
|
45
47
|
)
|
|
46
48
|
""",
|
|
47
49
|
"""
|
|
50
|
+
CREATE TABLE IF NOT EXISTS request_usage (
|
|
51
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
52
|
+
group_id INTEGER NOT NULL REFERENCES groups(group_id) ON DELETE CASCADE,
|
|
53
|
+
requested_at REAL NOT NULL
|
|
54
|
+
)
|
|
55
|
+
""",
|
|
56
|
+
"""
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_request_usage_group_time
|
|
58
|
+
ON request_usage(group_id, requested_at)
|
|
59
|
+
""",
|
|
60
|
+
"""
|
|
48
61
|
CREATE TABLE IF NOT EXISTS chunks (
|
|
49
62
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
50
63
|
group_id INTEGER NOT NULL REFERENCES groups(group_id) ON DELETE CASCADE,
|
|
@@ -104,6 +117,7 @@ class MemoryStore:
|
|
|
104
117
|
self._config = config
|
|
105
118
|
self._dimensions = config.group_chat_memory_embedding_dimensions
|
|
106
119
|
self._default_probability = config.group_chat_memory_default_probability
|
|
120
|
+
self._default_hourly_limit = config.group_chat_memory_hourly_request_limit
|
|
107
121
|
self._db: aiosqlite.Connection | None = None
|
|
108
122
|
self._use_sqlite_vec = False
|
|
109
123
|
self._lock = asyncio.Lock()
|
|
@@ -147,6 +161,7 @@ class MemoryStore:
|
|
|
147
161
|
async with self._transaction_on(db) as tx:
|
|
148
162
|
for statement in _SCHEMA_STATEMENTS:
|
|
149
163
|
await tx.execute(statement)
|
|
164
|
+
await self._ensure_groups_hourly_limit(tx)
|
|
150
165
|
await self._ensure_dimensions(tx)
|
|
151
166
|
if use_sqlite_vec:
|
|
152
167
|
await tx.execute(
|
|
@@ -243,6 +258,88 @@ class MemoryStore:
|
|
|
243
258
|
(group_id, probability, _format_timestamp(_utc_now())),
|
|
244
259
|
)
|
|
245
260
|
|
|
261
|
+
async def get_hourly_limit(self, group_id: int) -> int:
|
|
262
|
+
"""返回当前群每小时模型请求上限。"""
|
|
263
|
+
async with self._lock:
|
|
264
|
+
db = self._require_db()
|
|
265
|
+
cursor = await db.execute(
|
|
266
|
+
"SELECT hourly_limit FROM groups WHERE group_id = ?", (group_id,)
|
|
267
|
+
)
|
|
268
|
+
row = await cursor.fetchone()
|
|
269
|
+
await cursor.close()
|
|
270
|
+
if row is None:
|
|
271
|
+
return self._default_hourly_limit
|
|
272
|
+
return int(row["hourly_limit"])
|
|
273
|
+
|
|
274
|
+
async def set_hourly_limit(self, group_id: int, value: int) -> None:
|
|
275
|
+
"""设置当前群每小时模型请求上限,范围 1 到 10000。"""
|
|
276
|
+
limit = int(value)
|
|
277
|
+
if not 1 <= limit <= 10000:
|
|
278
|
+
raise ValueError("每小时请求上限必须是 1 到 10000 的整数。")
|
|
279
|
+
async with self._transaction() as db:
|
|
280
|
+
await db.execute(
|
|
281
|
+
"INSERT INTO groups(group_id, probability, hourly_limit, updated_at) "
|
|
282
|
+
"VALUES (?, ?, ?, ?) ON CONFLICT(group_id) DO UPDATE SET "
|
|
283
|
+
"hourly_limit = excluded.hourly_limit, updated_at = excluded.updated_at",
|
|
284
|
+
(
|
|
285
|
+
group_id,
|
|
286
|
+
self._default_probability,
|
|
287
|
+
limit,
|
|
288
|
+
_format_timestamp(_utc_now()),
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
async def consume_request_slot(
|
|
293
|
+
self, group_id: int, now: datetime | None = None
|
|
294
|
+
) -> bool:
|
|
295
|
+
"""在滚动 60 分钟窗口内尝试占用一次模型请求名额。"""
|
|
296
|
+
current = _as_utc(now or _utc_now())
|
|
297
|
+
timestamp = current.timestamp()
|
|
298
|
+
cutoff = (current - timedelta(hours=1)).timestamp()
|
|
299
|
+
async with self._transaction() as db:
|
|
300
|
+
await self._ensure_group(db, group_id)
|
|
301
|
+
await db.execute(
|
|
302
|
+
"DELETE FROM request_usage WHERE group_id = ? AND requested_at <= ?",
|
|
303
|
+
(group_id, cutoff),
|
|
304
|
+
)
|
|
305
|
+
cursor = await db.execute(
|
|
306
|
+
"SELECT count(*) AS usage FROM request_usage WHERE group_id = ?",
|
|
307
|
+
(group_id,),
|
|
308
|
+
)
|
|
309
|
+
row = await cursor.fetchone()
|
|
310
|
+
await cursor.close()
|
|
311
|
+
cursor = await db.execute(
|
|
312
|
+
"SELECT hourly_limit FROM groups WHERE group_id = ?", (group_id,)
|
|
313
|
+
)
|
|
314
|
+
group_row = await cursor.fetchone()
|
|
315
|
+
await cursor.close()
|
|
316
|
+
limit = (
|
|
317
|
+
int(group_row["hourly_limit"])
|
|
318
|
+
if group_row is not None
|
|
319
|
+
else self._default_hourly_limit
|
|
320
|
+
)
|
|
321
|
+
if row is not None and int(row["usage"]) >= limit:
|
|
322
|
+
return False
|
|
323
|
+
await db.execute(
|
|
324
|
+
"INSERT INTO request_usage(group_id, requested_at) VALUES (?, ?)",
|
|
325
|
+
(group_id, timestamp),
|
|
326
|
+
)
|
|
327
|
+
return True
|
|
328
|
+
|
|
329
|
+
async def get_hour_usage(self, group_id: int, now: datetime | None = None) -> int:
|
|
330
|
+
"""返回当前群滚动 60 分钟窗口内已占用的请求次数。"""
|
|
331
|
+
cutoff = (_as_utc(now or _utc_now()) - timedelta(hours=1)).timestamp()
|
|
332
|
+
async with self._lock:
|
|
333
|
+
db = self._require_db()
|
|
334
|
+
cursor = await db.execute(
|
|
335
|
+
"SELECT count(*) AS usage FROM request_usage "
|
|
336
|
+
"WHERE group_id = ? AND requested_at > ?",
|
|
337
|
+
(group_id, cutoff),
|
|
338
|
+
)
|
|
339
|
+
row = await cursor.fetchone()
|
|
340
|
+
await cursor.close()
|
|
341
|
+
return int(row["usage"]) if row is not None else 0
|
|
342
|
+
|
|
246
343
|
async def get_runtime_config(self) -> dict[str, str]:
|
|
247
344
|
"""读取全 Bot 共用的模型运行时配置。"""
|
|
248
345
|
async with self._lock:
|
|
@@ -397,13 +494,14 @@ class MemoryStore:
|
|
|
397
494
|
return [_row_to_chunk(row) for _, _, row in scored[:limit]]
|
|
398
495
|
|
|
399
496
|
async def clear_group(self, group_id: int) -> None:
|
|
400
|
-
"""
|
|
497
|
+
"""删除当前群的消息、片段、向量和请求计数,保留概率与每小时上限。"""
|
|
401
498
|
async with self._transaction() as db:
|
|
402
499
|
await db.execute(
|
|
403
500
|
"DELETE FROM memory_vector_blobs WHERE group_id = ?", (group_id,)
|
|
404
501
|
)
|
|
405
502
|
if self._use_sqlite_vec:
|
|
406
503
|
await db.execute("DELETE FROM memory_vectors WHERE group_id = ?", (group_id,))
|
|
504
|
+
await db.execute("DELETE FROM request_usage WHERE group_id = ?", (group_id,))
|
|
407
505
|
await db.execute("DELETE FROM messages WHERE group_id = ?", (group_id,))
|
|
408
506
|
await db.execute("DELETE FROM chunks WHERE group_id = ?", (group_id,))
|
|
409
507
|
|
|
@@ -433,11 +531,26 @@ class MemoryStore:
|
|
|
433
531
|
|
|
434
532
|
async def _ensure_group(self, db: aiosqlite.Connection, group_id: int) -> None:
|
|
435
533
|
await db.execute(
|
|
436
|
-
"INSERT INTO groups(group_id, probability, updated_at)
|
|
437
|
-
"ON CONFLICT(group_id) DO NOTHING",
|
|
438
|
-
(
|
|
534
|
+
"INSERT INTO groups(group_id, probability, hourly_limit, updated_at) "
|
|
535
|
+
"VALUES (?, ?, ?, ?) ON CONFLICT(group_id) DO NOTHING",
|
|
536
|
+
(
|
|
537
|
+
group_id,
|
|
538
|
+
self._default_probability,
|
|
539
|
+
self._default_hourly_limit,
|
|
540
|
+
_format_timestamp(_utc_now()),
|
|
541
|
+
),
|
|
439
542
|
)
|
|
440
543
|
|
|
544
|
+
async def _ensure_groups_hourly_limit(self, db: aiosqlite.Connection) -> None:
|
|
545
|
+
cursor = await db.execute("PRAGMA table_info(groups)")
|
|
546
|
+
rows = await cursor.fetchall()
|
|
547
|
+
await cursor.close()
|
|
548
|
+
if not any(row["name"] == "hourly_limit" for row in rows):
|
|
549
|
+
await db.execute(
|
|
550
|
+
f"ALTER TABLE groups ADD COLUMN hourly_limit INTEGER NOT NULL DEFAULT "
|
|
551
|
+
f"{self._default_hourly_limit}"
|
|
552
|
+
)
|
|
553
|
+
|
|
441
554
|
async def _ensure_dimensions(self, db: aiosqlite.Connection) -> None:
|
|
442
555
|
cursor = await db.execute("SELECT value FROM meta WHERE key = ?", (DIMENSIONS_META_KEY,))
|
|
443
556
|
row = await cursor.fetchone()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|