nonebot-plugin-group-chat-memory 0.1.0__tar.gz → 0.1.2__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.0 → nonebot_plugin_group_chat_memory-0.1.2}/PKG-INFO +6 -2
  2. {nonebot_plugin_group_chat_memory-0.1.0 → nonebot_plugin_group_chat_memory-0.1.2}/README.md +5 -1
  3. {nonebot_plugin_group_chat_memory-0.1.0 → nonebot_plugin_group_chat_memory-0.1.2}/nonebot_plugin_group_chat_memory/api.py +38 -7
  4. {nonebot_plugin_group_chat_memory-0.1.0 → nonebot_plugin_group_chat_memory-0.1.2}/nonebot_plugin_group_chat_memory/handlers.py +6 -3
  5. {nonebot_plugin_group_chat_memory-0.1.0 → nonebot_plugin_group_chat_memory-0.1.2}/nonebot_plugin_group_chat_memory/store.py +125 -18
  6. {nonebot_plugin_group_chat_memory-0.1.0 → nonebot_plugin_group_chat_memory-0.1.2}/pyproject.toml +1 -1
  7. {nonebot_plugin_group_chat_memory-0.1.0 → nonebot_plugin_group_chat_memory-0.1.2}/LICENSE +0 -0
  8. {nonebot_plugin_group_chat_memory-0.1.0 → nonebot_plugin_group_chat_memory-0.1.2}/nonebot_plugin_group_chat_memory/__init__.py +0 -0
  9. {nonebot_plugin_group_chat_memory-0.1.0 → nonebot_plugin_group_chat_memory-0.1.2}/nonebot_plugin_group_chat_memory/config.py +0 -0
  10. {nonebot_plugin_group_chat_memory-0.1.0 → nonebot_plugin_group_chat_memory-0.1.2}/nonebot_plugin_group_chat_memory/context.py +0 -0
  11. {nonebot_plugin_group_chat_memory-0.1.0 → nonebot_plugin_group_chat_memory-0.1.2}/nonebot_plugin_group_chat_memory/memory.py +0 -0
  12. {nonebot_plugin_group_chat_memory-0.1.0 → nonebot_plugin_group_chat_memory-0.1.2}/nonebot_plugin_group_chat_memory/models.py +0 -0
  13. {nonebot_plugin_group_chat_memory-0.1.0 → nonebot_plugin_group_chat_memory-0.1.2}/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.0
3
+ Version: 0.1.2
4
4
  Summary: 为 NoneBot2 群聊提供共享长期记忆、点名对话和按群概率回复
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -26,7 +26,7 @@ Description-Content-Type: text/markdown
26
26
 
27
27
  # nonebot-plugin-group-chat-memory
28
28
 
29
- 为 NoneBot2 OneBot v11 群聊提供按群隔离的长期向量记忆、@/回复对话和按群概率随机回复。插件使用 SQLite + sqlite-vec 保存原文、分块、向量和群配置,聊天与 Embedding 均请求可配置的 OpenAI 兼容 HTTP API。
29
+ 为 NoneBot2 OneBot v11 群聊提供按群隔离的长期向量记忆、@/回复对话和按群概率随机回复。插件使用 SQLite 保存原文、分块、向量和群配置,聊天与 Embedding 均请求可配置的 OpenAI 兼容 HTTP API。
30
30
 
31
31
  ## 环境与安装
32
32
 
@@ -34,6 +34,8 @@ Description-Content-Type: text/markdown
34
34
  - NoneBot2 2.2+
35
35
  - OneBot v11 适配器与对应的 OneBot 协议端
36
36
 
37
+ 优先使用 sqlite-vec 原生向量索引。如果当前 Python 的 `sqlite3` 不支持加载扩展(例如部分 python.org/Homebrew Python 3.13/3.14 构建),插件会自动改用 SQLite BLOB 保存向量并在 Python 中计算余弦相似度;启动日志会给出提示,功能保持可用。旧 sqlite-vec 数据库中缺少 BLOB 向量的已就绪片段会自动重新排队向量化。
38
+
37
39
  选择一种安装方式:
38
40
 
39
41
  ```bash
@@ -83,6 +85,8 @@ COMMAND_START=["/"]
83
85
 
84
86
  聊天地址填到 API 根路径(通常包含 `/v1`),插件分别请求其下的 `/chat/completions` 和 `/embeddings`。默认聊天模型标识是 `deepseek-v4.1-flash`,但**模型标识必须以实际服务商支持的值为准**。这四项保存到 SQLite,不再读取同名环境变量,更新后立即生效且重启后保留。使用 `/模型配置` 可查询脱敏状态。
85
87
 
88
+ 使用 OpenCode Go 直连 API 时,地址是 `https://opencode.ai/inference/go/openai/v1`,模型是 `deepseek-v4.1-flash`。插件会发送自己的 User-Agent,并对 OpenCode 请求附带按群稳定的 `x-opencode-session`,避免被网关当作通用 SDK 流量。
89
+
86
90
  ## 其余环境配置字段
87
91
 
88
92
  四个模型运行时字段不再通过 env 设置。其余以下字段可写在 NoneBot `.env` 中;集合类值使用 JSON 格式。
@@ -1,6 +1,6 @@
1
1
  # nonebot-plugin-group-chat-memory
2
2
 
3
- 为 NoneBot2 OneBot v11 群聊提供按群隔离的长期向量记忆、@/回复对话和按群概率随机回复。插件使用 SQLite + sqlite-vec 保存原文、分块、向量和群配置,聊天与 Embedding 均请求可配置的 OpenAI 兼容 HTTP API。
3
+ 为 NoneBot2 OneBot v11 群聊提供按群隔离的长期向量记忆、@/回复对话和按群概率随机回复。插件使用 SQLite 保存原文、分块、向量和群配置,聊天与 Embedding 均请求可配置的 OpenAI 兼容 HTTP API。
4
4
 
5
5
  ## 环境与安装
6
6
 
@@ -8,6 +8,8 @@
8
8
  - NoneBot2 2.2+
9
9
  - OneBot v11 适配器与对应的 OneBot 协议端
10
10
 
11
+ 优先使用 sqlite-vec 原生向量索引。如果当前 Python 的 `sqlite3` 不支持加载扩展(例如部分 python.org/Homebrew Python 3.13/3.14 构建),插件会自动改用 SQLite BLOB 保存向量并在 Python 中计算余弦相似度;启动日志会给出提示,功能保持可用。旧 sqlite-vec 数据库中缺少 BLOB 向量的已就绪片段会自动重新排队向量化。
12
+
11
13
  选择一种安装方式:
12
14
 
13
15
  ```bash
@@ -57,6 +59,8 @@ COMMAND_START=["/"]
57
59
 
58
60
  聊天地址填到 API 根路径(通常包含 `/v1`),插件分别请求其下的 `/chat/completions` 和 `/embeddings`。默认聊天模型标识是 `deepseek-v4.1-flash`,但**模型标识必须以实际服务商支持的值为准**。这四项保存到 SQLite,不再读取同名环境变量,更新后立即生效且重启后保留。使用 `/模型配置` 可查询脱敏状态。
59
61
 
62
+ 使用 OpenCode Go 直连 API 时,地址是 `https://opencode.ai/inference/go/openai/v1`,模型是 `deepseek-v4.1-flash`。插件会发送自己的 User-Agent,并对 OpenCode 请求附带按群稳定的 `x-opencode-session`,避免被网关当作通用 SDK 流量。
63
+
60
64
  ## 其余环境配置字段
61
65
 
62
66
  四个模型运行时字段不再通过 env 设置。其余以下字段可写在 NoneBot `.env` 中;集合类值使用 JSON 格式。
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import math
5
5
  from typing import Any
6
+ from urllib.parse import urlparse
6
7
 
7
8
  import httpx
8
9
 
@@ -11,6 +12,7 @@ from .context import build_chat_messages
11
12
  from .models import ChatContext
12
13
 
13
14
  CHAT_RETRY_DELAYS: tuple[float, ...] = (0.5, 1.0)
15
+ CLIENT_USER_AGENT = "nonebot-plugin-group-chat-memory/0.1.2"
14
16
 
15
17
 
16
18
  class ModelAPIError(RuntimeError):
@@ -24,9 +26,16 @@ class EmbeddingAPIError(ModelAPIError):
24
26
  class ChatAPIError(ModelAPIError):
25
27
  """聊天请求失败或响应不可用。"""
26
28
 
27
- def __init__(self, message: str, *, retryable: bool = False) -> None:
29
+ def __init__(
30
+ self,
31
+ message: str,
32
+ *,
33
+ retryable: bool = False,
34
+ status_code: int | None = None,
35
+ ) -> None:
28
36
  super().__init__(message)
29
37
  self.retryable = retryable
38
+ self.status_code = status_code
30
39
 
31
40
 
32
41
  def _make_client(timeout: float) -> httpx.AsyncClient:
@@ -39,7 +48,19 @@ def _endpoint(base_url: str, path: str) -> str:
39
48
 
40
49
 
41
50
  def _auth_headers(api_key: str) -> dict[str, str]:
42
- return {"Authorization": f"Bearer {api_key.strip()}"}
51
+ return {
52
+ "Authorization": f"Bearer {api_key.strip()}",
53
+ "User-Agent": CLIENT_USER_AGENT,
54
+ }
55
+
56
+
57
+ def _chat_headers(
58
+ base_url: str, api_key: str, session_id: str | None
59
+ ) -> dict[str, str]:
60
+ headers = _auth_headers(api_key)
61
+ if session_id and urlparse(base_url).hostname == "opencode.ai":
62
+ headers["x-opencode-session"] = session_id
63
+ return headers
43
64
 
44
65
 
45
66
  class EmbeddingClient:
@@ -105,7 +126,9 @@ class ChatClient:
105
126
  self._config = config
106
127
  self._retry_delays = tuple(retry_delays)
107
128
 
108
- async def complete(self, context: ChatContext, current_text: str) -> str:
129
+ async def complete(
130
+ self, context: ChatContext, current_text: str, *, session_id: str | None = None
131
+ ) -> str:
109
132
  """按上下文请求一条回复;瞬时失败按配置次数重试,最终失败抛出 ChatAPIError。"""
110
133
  self._config.require_chat_config()
111
134
  if not current_text.strip():
@@ -118,7 +141,7 @@ class ChatClient:
118
141
  attempts = len(self._retry_delays) + 1
119
142
  for attempt in range(attempts):
120
143
  try:
121
- response = await self._post(payload)
144
+ response = await self._post(payload, session_id)
122
145
  except ChatAPIError as exc:
123
146
  if not exc.retryable or attempt + 1 >= attempts:
124
147
  raise
@@ -127,9 +150,16 @@ class ChatClient:
127
150
  return self._parse(response)
128
151
  raise ChatAPIError("聊天请求重试次数已用尽。")
129
152
 
130
- async def _post(self, payload: dict[str, Any]) -> httpx.Response:
131
- url = _endpoint(self._config.group_chat_memory_chat_base_url, "chat/completions")
132
- headers = _auth_headers(self._config.group_chat_memory_chat_api_key.get_secret_value())
153
+ async def _post(
154
+ self, payload: dict[str, Any], session_id: str | None
155
+ ) -> httpx.Response:
156
+ base_url = self._config.group_chat_memory_chat_base_url
157
+ url = _endpoint(base_url, "chat/completions")
158
+ headers = _chat_headers(
159
+ base_url,
160
+ self._config.group_chat_memory_chat_api_key.get_secret_value(),
161
+ session_id,
162
+ )
133
163
  timeout = self._config.group_chat_memory_chat_timeout
134
164
  try:
135
165
  async with _make_client(timeout) as client:
@@ -142,6 +172,7 @@ class ChatClient:
142
172
  raise ChatAPIError(
143
173
  f"聊天服务返回 HTTP {response.status_code}。",
144
174
  retryable=response.status_code == 429 or response.status_code >= 500,
175
+ status_code=response.status_code,
145
176
  )
146
177
  return response
147
178
 
@@ -21,7 +21,7 @@ from nonebot.adapters.onebot.v11 import (
21
21
  from nonebot.params import CommandArg
22
22
  from nonebot.permission import SUPERUSER
23
23
 
24
- from .api import ChatClient, EmbeddingClient
24
+ from .api import ChatAPIError, ChatClient, EmbeddingClient
25
25
  from .config import get_settings
26
26
  from .context import ContextBuilder
27
27
  from .memory import MemoryWriter
@@ -79,9 +79,12 @@ async def reply(group_id: int, message: MemoryMessage) -> None:
79
79
  bot = _bots[group_id]
80
80
  try:
81
81
  context = await context_builder.build(group_id, message.text)
82
- response = await chat_client.complete(context, message.text)
82
+ response = await chat_client.complete(
83
+ context, message.text, session_id=f"group:{group_id}"
84
+ )
83
85
  except Exception as exc:
84
- logger.error("群 %s 的聊天回复失败:%s", group_id, type(exc).__name__)
86
+ status = f" HTTP {exc.status_code}" if isinstance(exc, ChatAPIError) else ""
87
+ logger.error("群 %s 的聊天回复失败:%s%s", group_id, type(exc).__name__, status)
85
88
  await bot.send_group_msg(group_id=group_id, message=GENERIC_REPLY_ERROR)
86
89
  return
87
90
 
@@ -1,9 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ import logging
4
5
  import math
6
+ import sqlite3
7
+ import struct
5
8
  from collections.abc import AsyncIterator, Sequence
6
- from contextlib import asynccontextmanager
9
+ from contextlib import asynccontextmanager, suppress
7
10
  from datetime import datetime, timezone
8
11
 
9
12
  import aiosqlite
@@ -12,8 +15,14 @@ import sqlite_vec
12
15
  from .config import RUNTIME_CONFIG_DEFAULTS, Config
13
16
  from .models import Chunk, ChunkStatus, Message
14
17
 
18
+ logger = logging.getLogger(__name__)
19
+
15
20
  DIMENSIONS_META_KEY = "embedding_dimensions"
16
21
 
22
+
23
+ def _supports_load_extension() -> bool:
24
+ return hasattr(sqlite3.Connection, "enable_load_extension")
25
+
17
26
  _SCHEMA_STATEMENTS = (
18
27
  """
19
28
  CREATE TABLE IF NOT EXISTS meta (
@@ -66,6 +75,17 @@ _SCHEMA_STATEMENTS = (
66
75
  CREATE INDEX IF NOT EXISTS idx_messages_unchunked
67
76
  ON messages(group_id, id) WHERE chunk_id IS NULL
68
77
  """,
78
+ """
79
+ CREATE TABLE IF NOT EXISTS memory_vector_blobs (
80
+ chunk_id INTEGER PRIMARY KEY REFERENCES chunks(id) ON DELETE CASCADE,
81
+ group_id INTEGER NOT NULL,
82
+ embedding BLOB NOT NULL
83
+ )
84
+ """,
85
+ """
86
+ CREATE INDEX IF NOT EXISTS idx_vector_blobs_group
87
+ ON memory_vector_blobs(group_id)
88
+ """,
69
89
  )
70
90
 
71
91
  _VECTOR_TABLE_STATEMENT = """
@@ -85,10 +105,11 @@ class MemoryStore:
85
105
  self._dimensions = config.group_chat_memory_embedding_dimensions
86
106
  self._default_probability = config.group_chat_memory_default_probability
87
107
  self._db: aiosqlite.Connection | None = None
108
+ self._use_sqlite_vec = False
88
109
  self._lock = asyncio.Lock()
89
110
 
90
111
  async def open(self) -> None:
91
- """打开数据库、加载 sqlite-vec 并创建缺失的表结构。"""
112
+ """打开数据库;优先加载 sqlite-vec,不支持时使用 BLOB 向量回退。"""
92
113
  async with self._lock:
93
114
  if self._db is not None:
94
115
  return
@@ -96,23 +117,57 @@ class MemoryStore:
96
117
  path = self._config.database_path
97
118
  path.parent.mkdir(parents=True, exist_ok=True)
98
119
  db = await aiosqlite.connect(path, isolation_level=None)
120
+ use_sqlite_vec = _supports_load_extension()
99
121
  try:
100
122
  db.row_factory = aiosqlite.Row
101
- await db.enable_load_extension(True)
102
- await db.load_extension(sqlite_vec.loadable_path())
103
- await db.enable_load_extension(False)
123
+ if use_sqlite_vec:
124
+ try:
125
+ await db.enable_load_extension(True)
126
+ await db.load_extension(sqlite_vec.loadable_path())
127
+ except Exception as exc:
128
+ logger.warning(
129
+ "sqlite-vec 加载失败,改用 SQLite BLOB 向量检索:%s",
130
+ type(exc).__name__,
131
+ )
132
+ use_sqlite_vec = False
133
+ finally:
134
+ if hasattr(db, "enable_load_extension"):
135
+ with suppress(Exception):
136
+ await db.enable_load_extension(False)
137
+ else:
138
+ logger.warning(
139
+ "当前 Python 的 sqlite3 不支持加载扩展,改用 SQLite BLOB 向量检索。"
140
+ )
104
141
  await db.execute("PRAGMA journal_mode = WAL")
105
142
  await db.execute("PRAGMA foreign_keys = ON")
106
143
  await db.execute("PRAGMA busy_timeout = 5000")
107
144
  await db.execute("PRAGMA synchronous = NORMAL")
108
145
  self._db = db
146
+ self._use_sqlite_vec = use_sqlite_vec
109
147
  async with self._transaction_on(db) as tx:
110
148
  for statement in _SCHEMA_STATEMENTS:
111
149
  await tx.execute(statement)
112
150
  await self._ensure_dimensions(tx)
113
- await tx.execute(_VECTOR_TABLE_STATEMENT.format(dimensions=self._dimensions))
151
+ if use_sqlite_vec:
152
+ await tx.execute(
153
+ _VECTOR_TABLE_STATEMENT.format(dimensions=self._dimensions)
154
+ )
155
+ else:
156
+ cursor = await tx.execute(
157
+ "UPDATE chunks SET status = ? WHERE status = ? AND NOT EXISTS ("
158
+ "SELECT 1 FROM memory_vector_blobs AS b WHERE b.chunk_id = chunks.id"
159
+ ")",
160
+ (ChunkStatus.PENDING.value, ChunkStatus.READY.value),
161
+ )
162
+ if cursor.rowcount > 0:
163
+ logger.warning(
164
+ "%s 个片段缺少 BLOB 向量,已重新排队等待向量化。",
165
+ cursor.rowcount,
166
+ )
167
+ await cursor.close()
114
168
  except BaseException:
115
169
  self._db = None
170
+ self._use_sqlite_vec = False
116
171
  await db.close()
117
172
  raise
118
173
 
@@ -282,11 +337,21 @@ class MemoryStore:
282
337
  await cursor.close()
283
338
  if row is None:
284
339
  return
285
- await db.execute("DELETE FROM memory_vectors WHERE chunk_id = ?", (chunk_id,))
286
- await db.execute(
287
- "INSERT INTO memory_vectors(chunk_id, group_id, embedding) VALUES (?, ?, ?)",
288
- (chunk_id, row["group_id"], sqlite_vec.serialize_float32(embedding)),
289
- )
340
+ if self._use_sqlite_vec:
341
+ await db.execute("DELETE FROM memory_vectors WHERE chunk_id = ?", (chunk_id,))
342
+ await db.execute(
343
+ "INSERT INTO memory_vectors(chunk_id, group_id, embedding) VALUES (?, ?, ?)",
344
+ (chunk_id, row["group_id"], sqlite_vec.serialize_float32(embedding)),
345
+ )
346
+ else:
347
+ await db.execute(
348
+ "DELETE FROM memory_vector_blobs WHERE chunk_id = ?", (chunk_id,)
349
+ )
350
+ await db.execute(
351
+ "INSERT INTO memory_vector_blobs(chunk_id, group_id, embedding) "
352
+ "VALUES (?, ?, ?)",
353
+ (chunk_id, row["group_id"], _pack_vector(embedding)),
354
+ )
290
355
  await db.execute(
291
356
  "UPDATE chunks SET status = ? WHERE id = ?",
292
357
  (ChunkStatus.READY.value, chunk_id),
@@ -299,21 +364,46 @@ class MemoryStore:
299
364
  return []
300
365
  async with self._lock:
301
366
  db = self._require_db()
367
+ if self._use_sqlite_vec:
368
+ cursor = await db.execute(
369
+ "SELECT c.id, c.group_id, c.text, c.status FROM ("
370
+ "SELECT chunk_id, distance FROM memory_vectors "
371
+ "WHERE embedding MATCH ? AND k = ? AND group_id = ?"
372
+ ") AS v JOIN chunks AS c ON c.id = v.chunk_id ORDER BY v.distance",
373
+ (sqlite_vec.serialize_float32(embedding), limit, group_id),
374
+ )
375
+ rows = await cursor.fetchall()
376
+ await cursor.close()
377
+ return [_row_to_chunk(row) for row in rows]
378
+
302
379
  cursor = await db.execute(
303
- "SELECT c.id, c.group_id, c.text, c.status FROM ("
304
- "SELECT chunk_id, distance FROM memory_vectors "
305
- "WHERE embedding MATCH ? AND k = ? AND group_id = ?"
306
- ") AS v JOIN chunks AS c ON c.id = v.chunk_id ORDER BY v.distance",
307
- (sqlite_vec.serialize_float32(embedding), limit, group_id),
380
+ "SELECT c.id, c.group_id, c.text, c.status, b.embedding "
381
+ "FROM memory_vector_blobs AS b JOIN chunks AS c ON c.id = b.chunk_id "
382
+ "WHERE b.group_id = ? AND c.status = ?",
383
+ (group_id, ChunkStatus.READY.value),
308
384
  )
309
385
  rows = await cursor.fetchall()
310
386
  await cursor.close()
311
- return [_row_to_chunk(row) for row in rows]
387
+ scored = [
388
+ (
389
+ _cosine_similarity(embedding, _unpack_vector(row["embedding"], self._dimensions)),
390
+ int(row["id"]),
391
+ row,
392
+ )
393
+ for row in rows
394
+ if len(row["embedding"]) == self._dimensions * 4
395
+ ]
396
+ scored.sort(key=lambda item: (-item[0], item[1]))
397
+ return [_row_to_chunk(row) for _, _, row in scored[:limit]]
312
398
 
313
399
  async def clear_group(self, group_id: int) -> None:
314
400
  """删除当前群的消息、片段与向量,保留概率配置。"""
315
401
  async with self._transaction() as db:
316
- await db.execute("DELETE FROM memory_vectors WHERE group_id = ?", (group_id,))
402
+ await db.execute(
403
+ "DELETE FROM memory_vector_blobs WHERE group_id = ?", (group_id,)
404
+ )
405
+ if self._use_sqlite_vec:
406
+ await db.execute("DELETE FROM memory_vectors WHERE group_id = ?", (group_id,))
317
407
  await db.execute("DELETE FROM messages WHERE group_id = ?", (group_id,))
318
408
  await db.execute("DELETE FROM chunks WHERE group_id = ?", (group_id,))
319
409
 
@@ -407,6 +497,23 @@ def _validated_vector(vector: list[float], dimensions: int) -> list[float]:
407
497
  return values
408
498
 
409
499
 
500
+ def _pack_vector(vector: list[float]) -> bytes:
501
+ return struct.pack(f"<{len(vector)}f", *vector)
502
+
503
+
504
+ def _unpack_vector(value: bytes, dimensions: int) -> list[float]:
505
+ return list(struct.unpack(f"<{dimensions}f", value))
506
+
507
+
508
+ def _cosine_similarity(left: list[float], right: list[float]) -> float:
509
+ dot = sum(a * b for a, b in zip(left, right, strict=True))
510
+ left_norm = math.sqrt(sum(value * value for value in left))
511
+ right_norm = math.sqrt(sum(value * value for value in right))
512
+ if left_norm == 0.0 or right_norm == 0.0:
513
+ return 0.0
514
+ return dot / (left_norm * right_norm)
515
+
516
+
410
517
  def _utc_now() -> datetime:
411
518
  return datetime.now(timezone.utc)
412
519
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nonebot-plugin-group-chat-memory"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  description = "为 NoneBot2 群聊提供共享长期记忆、点名对话和按群概率回复"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "TonyLiangP2010405" }]