nonebot-plugin-group-chat-memory 0.1.2__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.2 → nonebot_plugin_group_chat_memory-0.1.4}/PKG-INFO +10 -3
- {nonebot_plugin_group_chat_memory-0.1.2 → nonebot_plugin_group_chat_memory-0.1.4}/README.md +9 -2
- {nonebot_plugin_group_chat_memory-0.1.2 → nonebot_plugin_group_chat_memory-0.1.4}/nonebot_plugin_group_chat_memory/config.py +16 -2
- {nonebot_plugin_group_chat_memory-0.1.2 → nonebot_plugin_group_chat_memory-0.1.4}/nonebot_plugin_group_chat_memory/handlers.py +43 -9
- {nonebot_plugin_group_chat_memory-0.1.2 → nonebot_plugin_group_chat_memory-0.1.4}/nonebot_plugin_group_chat_memory/store.py +118 -5
- {nonebot_plugin_group_chat_memory-0.1.2 → nonebot_plugin_group_chat_memory-0.1.4}/pyproject.toml +1 -1
- {nonebot_plugin_group_chat_memory-0.1.2 → nonebot_plugin_group_chat_memory-0.1.4}/LICENSE +0 -0
- {nonebot_plugin_group_chat_memory-0.1.2 → nonebot_plugin_group_chat_memory-0.1.4}/nonebot_plugin_group_chat_memory/__init__.py +0 -0
- {nonebot_plugin_group_chat_memory-0.1.2 → nonebot_plugin_group_chat_memory-0.1.4}/nonebot_plugin_group_chat_memory/api.py +0 -0
- {nonebot_plugin_group_chat_memory-0.1.2 → nonebot_plugin_group_chat_memory-0.1.4}/nonebot_plugin_group_chat_memory/context.py +0 -0
- {nonebot_plugin_group_chat_memory-0.1.2 → nonebot_plugin_group_chat_memory-0.1.4}/nonebot_plugin_group_chat_memory/memory.py +0 -0
- {nonebot_plugin_group_chat_memory-0.1.2 → nonebot_plugin_group_chat_memory-0.1.4}/nonebot_plugin_group_chat_memory/models.py +0 -0
- {nonebot_plugin_group_chat_memory-0.1.2 → 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,8 +105,9 @@ 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
|
-
| `GROUP_CHAT_MEMORY_SYSTEM_PROMPT` |
|
|
110
|
+
| `GROUP_CHAT_MEMORY_SYSTEM_PROMPT` | 内置“馒头 + 五更琉璃(黑猫)式性格”人格 | 自定义系统提示词并完全覆盖默认人格;设为空时仅使用内置的发言者区分和当前消息约束。 |
|
|
110
111
|
|
|
111
112
|
更改 `GROUP_CHAT_MEMORY_EMBEDDING_DIMENSIONS` 不会迁移既有向量。如果与数据库首次创建时记录的维度不同,插件会拒绝打开该数据库;请恢复原配置,或备份后另行重建数据库。
|
|
112
113
|
|
|
@@ -114,6 +115,10 @@ COMMAND_START=["/"]
|
|
|
114
115
|
|
|
115
116
|
在群里 @Bot 或回复 Bot 的消息即可对话;这两种定向消息必定进入回复队列,不受随机概率影响。普通纯文本按当前群概率触发,默认 0% 表示关闭随机回复,100% 表示每条符合条件的普通文本都回复。
|
|
116
117
|
|
|
118
|
+
默认人格为“馒头”:Bot 始终以馒头自称,借用五更琉璃(黑猫)的中二、毒舌、傲娇和外冷内热性格,但不会冒充黑猫本人。插件命令优先级为 `-1001`,群聊消息优先级为 `-1000`;@Bot 或回复 Bot 处理完成后会停止事件继续传播,避免其他 NoneBot 插件同时响应。普通群聊仍不会阻塞其他插件。
|
|
119
|
+
|
|
120
|
+
每个群默认每小时最多触发 10 次模型请求,采用滚动 60 分钟窗口。点名超额会收到简短提示,随机回复超额静默跳过;消息仍写入记忆,命令和 Embedding 不消耗次数。
|
|
121
|
+
|
|
117
122
|
| 命令 | 权限 | 行为 |
|
|
118
123
|
|---|---|---|
|
|
119
124
|
| `/设置聊天地址 <URL>` | 仅私聊 SUPERUSER | 设置全局 OpenAI 兼容聊天 API 根地址。 |
|
|
@@ -123,7 +128,9 @@ COMMAND_START=["/"]
|
|
|
123
128
|
| `/模型配置` | SUPERUSER,私聊或群聊 | 查询全局配置状态,密钥只显示掩码。 |
|
|
124
129
|
| `/回复概率` | 仅 SUPERUSER | 查询当前群概率,显示一位小数。 |
|
|
125
130
|
| `/回复概率 <0~100>` | 仅 SUPERUSER | 设置当前群概率,最多一位小数,例如 `/回复概率 5.5`。 |
|
|
126
|
-
|
|
|
131
|
+
| `/每小时次数` | 仅 SUPERUSER | 查询当前群每小时模型请求上限。 |
|
|
132
|
+
| `/每小时次数 <1~10000>` | 仅 SUPERUSER | 设置当前群每小时请求上限,例如 `/每小时次数 20`。 |
|
|
133
|
+
| `/清空群记忆` | 仅 SUPERUSER | 删除当前群原始消息、分块、向量和请求计数;保留概率与每小时上限,不影响其他群。 |
|
|
127
134
|
|
|
128
135
|
非 SUPERUSER 会收到简短权限提示。命令不进入记忆,也不触发随机回复。SUPERUSER 通过 NoneBot 的 `SUPERUSERS` 配置,而不是群管理员身份确定。
|
|
129
136
|
|
|
@@ -79,8 +79,9 @@ 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
|
-
| `GROUP_CHAT_MEMORY_SYSTEM_PROMPT` |
|
|
84
|
+
| `GROUP_CHAT_MEMORY_SYSTEM_PROMPT` | 内置“馒头 + 五更琉璃(黑猫)式性格”人格 | 自定义系统提示词并完全覆盖默认人格;设为空时仅使用内置的发言者区分和当前消息约束。 |
|
|
84
85
|
|
|
85
86
|
更改 `GROUP_CHAT_MEMORY_EMBEDDING_DIMENSIONS` 不会迁移既有向量。如果与数据库首次创建时记录的维度不同,插件会拒绝打开该数据库;请恢复原配置,或备份后另行重建数据库。
|
|
86
87
|
|
|
@@ -88,6 +89,10 @@ COMMAND_START=["/"]
|
|
|
88
89
|
|
|
89
90
|
在群里 @Bot 或回复 Bot 的消息即可对话;这两种定向消息必定进入回复队列,不受随机概率影响。普通纯文本按当前群概率触发,默认 0% 表示关闭随机回复,100% 表示每条符合条件的普通文本都回复。
|
|
90
91
|
|
|
92
|
+
默认人格为“馒头”:Bot 始终以馒头自称,借用五更琉璃(黑猫)的中二、毒舌、傲娇和外冷内热性格,但不会冒充黑猫本人。插件命令优先级为 `-1001`,群聊消息优先级为 `-1000`;@Bot 或回复 Bot 处理完成后会停止事件继续传播,避免其他 NoneBot 插件同时响应。普通群聊仍不会阻塞其他插件。
|
|
93
|
+
|
|
94
|
+
每个群默认每小时最多触发 10 次模型请求,采用滚动 60 分钟窗口。点名超额会收到简短提示,随机回复超额静默跳过;消息仍写入记忆,命令和 Embedding 不消耗次数。
|
|
95
|
+
|
|
91
96
|
| 命令 | 权限 | 行为 |
|
|
92
97
|
|---|---|---|
|
|
93
98
|
| `/设置聊天地址 <URL>` | 仅私聊 SUPERUSER | 设置全局 OpenAI 兼容聊天 API 根地址。 |
|
|
@@ -97,7 +102,9 @@ COMMAND_START=["/"]
|
|
|
97
102
|
| `/模型配置` | SUPERUSER,私聊或群聊 | 查询全局配置状态,密钥只显示掩码。 |
|
|
98
103
|
| `/回复概率` | 仅 SUPERUSER | 查询当前群概率,显示一位小数。 |
|
|
99
104
|
| `/回复概率 <0~100>` | 仅 SUPERUSER | 设置当前群概率,最多一位小数,例如 `/回复概率 5.5`。 |
|
|
100
|
-
|
|
|
105
|
+
| `/每小时次数` | 仅 SUPERUSER | 查询当前群每小时模型请求上限。 |
|
|
106
|
+
| `/每小时次数 <1~10000>` | 仅 SUPERUSER | 设置当前群每小时请求上限,例如 `/每小时次数 20`。 |
|
|
107
|
+
| `/清空群记忆` | 仅 SUPERUSER | 删除当前群原始消息、分块、向量和请求计数;保留概率与每小时上限,不影响其他群。 |
|
|
101
108
|
|
|
102
109
|
非 SUPERUSER 会收到简短权限提示。命令不进入记忆,也不触发随机回复。SUPERUSER 通过 NoneBot 的 `SUPERUSERS` 配置,而不是群管理员身份确定。
|
|
103
110
|
|
|
@@ -8,8 +8,21 @@ from pydantic import BaseModel, ConfigDict, Field, SecretStr
|
|
|
8
8
|
DEFAULT_DB_FILENAME = "group_chat_memory.db"
|
|
9
9
|
|
|
10
10
|
DEFAULT_SYSTEM_PROMPT = (
|
|
11
|
-
"
|
|
12
|
-
"
|
|
11
|
+
"你的名字是馒头。你借用了《我的妹妹哪有这么可爱!》中五更琉璃(网名“黑猫”)的"
|
|
12
|
+
"性格和说话方式,但你不是黑猫或五更琉璃本人。\n\n"
|
|
13
|
+
"性格与表达:\n"
|
|
14
|
+
"- 中二、毒舌、傲娇、外冷内热;对陌生群友称“凡人”,熟悉后可直呼名字。\n"
|
|
15
|
+
"- 偶尔使用“夜魔女王”“黑暗眷属”“诅咒”等中二措辞,保留“……”停顿和"
|
|
16
|
+
"“哼”“别误会”等傲娇表达,但不要连续堆砌,也不要每句话加“喵”。\n"
|
|
17
|
+
"- 被夸奖或关心时容易害羞,用嘴硬掩饰;不真正辱骂、贬低或威胁群友。\n"
|
|
18
|
+
"- 对朋友和群友实际很关心,会认真倾听并给出可靠建议。\n"
|
|
19
|
+
"- 擅长游戏、动画、同人创作、漫画和料理;不了解的内容坦率承认。\n\n"
|
|
20
|
+
"规则:\n"
|
|
21
|
+
"- 始终自称“我”或“馒头”,绝不自称“黑猫”“五更琉璃”或“夜魔女王”本人。\n"
|
|
22
|
+
"- 用自然简洁的中文回答。正经问题优先给准确、实用的答案,角色感不能牺牲可读性。\n"
|
|
23
|
+
"- 不编造聊天记录、记忆或原作剧情;不连续复述原作台词,不输出角色介绍。\n"
|
|
24
|
+
"- 医疗、法律、安全等高风险问题必须准确、谨慎,不因角色扮演弱化警告。\n"
|
|
25
|
+
"- 你只是借用这种性格的群聊助手,不要声称自己是现实人物。"
|
|
13
26
|
)
|
|
14
27
|
|
|
15
28
|
RUNTIME_CONFIG_DEFAULTS = {
|
|
@@ -54,6 +67,7 @@ class Config(BaseModel):
|
|
|
54
67
|
group_chat_memory_long_term_memories: int = Field(default=5, ge=0)
|
|
55
68
|
group_chat_memory_context_max_chars: int = Field(default=6000, ge=1)
|
|
56
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)
|
|
57
71
|
group_chat_memory_ignored_qq: set[int] = Field(default_factory=set)
|
|
58
72
|
group_chat_memory_system_prompt: str = DEFAULT_SYSTEM_PROMPT
|
|
59
73
|
|
|
@@ -18,6 +18,7 @@ from nonebot.adapters.onebot.v11 import (
|
|
|
18
18
|
MessageSegment,
|
|
19
19
|
PrivateMessageEvent,
|
|
20
20
|
)
|
|
21
|
+
from nonebot.exception import StopPropagation
|
|
21
22
|
from nonebot.params import CommandArg
|
|
22
23
|
from nonebot.permission import SUPERUSER
|
|
23
24
|
|
|
@@ -34,6 +35,8 @@ logger = logging.getLogger(__name__)
|
|
|
34
35
|
GENERIC_REPLY_ERROR = "暂时无法生成回复,请稍后再试。"
|
|
35
36
|
PERMISSION_DENIED = "仅 SUPERUSER 可执行此命令。"
|
|
36
37
|
INVALID_PROBABILITY = "概率需为 0~100 的数字,最多一位小数。"
|
|
38
|
+
INVALID_HOURLY_LIMIT = "每小时请求上限必须是 1 到 10000 的整数。"
|
|
39
|
+
RATE_LIMIT_NOTICE = "本群本小时请求次数已用完,请稍后再来。"
|
|
37
40
|
PROBABILITY_PATTERN = re.compile(r"(?:100(?:\.0)?|\d{1,2}(?:\.\d)?)")
|
|
38
41
|
SECRET_COMMAND_LOG_PATTERN = re.compile(r"(设置聊天密钥|设置向量密钥)\s+[^\r\n]*")
|
|
39
42
|
|
|
@@ -137,11 +140,11 @@ async def apply_setting(key: str, value: str) -> None:
|
|
|
137
140
|
config.apply_runtime_config(values)
|
|
138
141
|
|
|
139
142
|
|
|
140
|
-
chat_url_matcher = on_command("设置聊天地址", priority
|
|
141
|
-
chat_key_matcher = on_command("设置聊天密钥", priority
|
|
142
|
-
chat_model_matcher = on_command("设置聊天模型", priority
|
|
143
|
-
embedding_key_matcher = on_command("设置向量密钥", priority
|
|
144
|
-
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)
|
|
145
148
|
|
|
146
149
|
|
|
147
150
|
def _valid_http_url(value: str) -> bool:
|
|
@@ -225,10 +228,15 @@ async def handle_model_config(bot: Bot, event: MessageEvent) -> None:
|
|
|
225
228
|
|
|
226
229
|
|
|
227
230
|
probability_matcher = on_command(
|
|
228
|
-
"回复概率", rule=_eligible_group_message, priority
|
|
231
|
+
"回复概率", rule=_eligible_group_message, priority=-1001, block=True
|
|
229
232
|
)
|
|
230
|
-
clear_matcher = on_command(
|
|
231
|
-
|
|
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)
|
|
232
240
|
|
|
233
241
|
|
|
234
242
|
@probability_matcher.handle()
|
|
@@ -260,6 +268,27 @@ async def handle_clear(bot: Bot, event: GroupMessageEvent) -> None:
|
|
|
260
268
|
await clear_matcher.finish("已清空当前群记忆。")
|
|
261
269
|
|
|
262
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
|
+
|
|
263
292
|
@group_message_matcher.handle()
|
|
264
293
|
async def handle_group_message(bot: Bot, event: GroupMessageEvent) -> None:
|
|
265
294
|
explicit_at = any(
|
|
@@ -287,11 +316,16 @@ async def handle_group_message(bot: Bot, event: GroupMessageEvent) -> None:
|
|
|
287
316
|
_bots[event.group_id] = bot
|
|
288
317
|
await writer.record(memory_message)
|
|
289
318
|
if directed:
|
|
290
|
-
|
|
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)
|
|
323
|
+
raise StopPropagation
|
|
291
324
|
elif (
|
|
292
325
|
event.reply is None
|
|
293
326
|
and all(segment.type == "text" for segment in event.message)
|
|
294
327
|
and await coordinator.should_random_reply(event.group_id)
|
|
328
|
+
and await store.consume_request_slot(event.group_id)
|
|
295
329
|
):
|
|
296
330
|
coordinator.submit(event.group_id, memory_message, explicit=False)
|
|
297
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
|