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.
Files changed (13) hide show
  1. {nonebot_plugin_group_chat_memory-0.1.3 → nonebot_plugin_group_chat_memory-0.1.4}/PKG-INFO +8 -3
  2. {nonebot_plugin_group_chat_memory-0.1.3 → nonebot_plugin_group_chat_memory-0.1.4}/README.md +7 -2
  3. {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
  4. {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
  5. {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
  6. {nonebot_plugin_group_chat_memory-0.1.3 → nonebot_plugin_group_chat_memory-0.1.4}/pyproject.toml +1 -1
  7. {nonebot_plugin_group_chat_memory-0.1.3 → nonebot_plugin_group_chat_memory-0.1.4}/LICENSE +0 -0
  8. {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
  9. {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
  10. {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
  11. {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
  12. {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
  13. {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
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 始终以馒头自称,借用五更琉璃(黑猫)的中二、毒舌、傲娇和外冷内热性格,但不会冒充黑猫本人。插件命令优先级为 `-1`,群聊消息优先级为 `0`;@Bot 或回复 Bot 处理完成后会停止事件继续传播,避免其他 NoneBot 插件同时响应。普通群聊仍不会阻塞其他插件。
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
- | `/清空群记忆` | 仅 SUPERUSER | 删除当前群原始消息、分块和向量;保留当前群概率,不影响其他群。 |
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 始终以馒头自称,借用五更琉璃(黑猫)的中二、毒舌、傲娇和外冷内热性格,但不会冒充黑猫本人。插件命令优先级为 `-1`,群聊消息优先级为 `0`;@Bot 或回复 Bot 处理完成后会停止事件继续传播,避免其他 NoneBot 插件同时响应。普通群聊仍不会阻塞其他插件。
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
- | `/清空群记忆` | 仅 SUPERUSER | 删除当前群原始消息、分块和向量;保留当前群概率,不影响其他群。 |
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=-1, block=True)
142
- chat_key_matcher = on_command("设置聊天密钥", priority=-1, block=True)
143
- chat_model_matcher = on_command("设置聊天模型", priority=-1, block=True)
144
- embedding_key_matcher = on_command("设置向量密钥", priority=-1, block=True)
145
- model_config_matcher = on_command("模型配置", priority=-1, block=True)
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=-1, block=True
231
+ "回复概率", rule=_eligible_group_message, priority=-1001, block=True
230
232
  )
231
- clear_matcher = on_command("清空群记忆", rule=_eligible_group_message, priority=-1, block=True)
232
- group_message_matcher = on_message(rule=_eligible_group_message, priority=0, block=False)
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
- coordinator.submit(event.group_id, memory_message, explicit=True)
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) VALUES (?, ?, ?) "
437
- "ON CONFLICT(group_id) DO NOTHING",
438
- (group_id, self._default_probability, _format_timestamp(_utc_now())),
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()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nonebot-plugin-group-chat-memory"
3
- version = "0.1.3"
3
+ version = "0.1.4"
4
4
  description = "为 NoneBot2 群聊提供共享长期记忆、点名对话和按群概率回复"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "TonyLiangP2010405" }]