nonebot-plugin-dotcharacter 2.0.5__tar.gz → 2.0.7__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_dotcharacter-2.0.5 → nonebot_plugin_dotcharacter-2.0.7}/PKG-INFO +1 -2
- {nonebot_plugin_dotcharacter-2.0.5 → nonebot_plugin_dotcharacter-2.0.7}/nonebot_plugin_dotcharacter/__init__.py +1 -18
- {nonebot_plugin_dotcharacter-2.0.5 → nonebot_plugin_dotcharacter-2.0.7}/nonebot_plugin_dotcharacter/conversation.py +184 -107
- {nonebot_plugin_dotcharacter-2.0.5 → nonebot_plugin_dotcharacter-2.0.7}/nonebot_plugin_dotcharacter.egg-info/PKG-INFO +1 -2
- {nonebot_plugin_dotcharacter-2.0.5 → nonebot_plugin_dotcharacter-2.0.7}/nonebot_plugin_dotcharacter.egg-info/requires.txt +0 -1
- {nonebot_plugin_dotcharacter-2.0.5 → nonebot_plugin_dotcharacter-2.0.7}/pyproject.toml +1 -2
- {nonebot_plugin_dotcharacter-2.0.5 → nonebot_plugin_dotcharacter-2.0.7}/LICENSE +0 -0
- {nonebot_plugin_dotcharacter-2.0.5 → nonebot_plugin_dotcharacter-2.0.7}/README.md +0 -0
- {nonebot_plugin_dotcharacter-2.0.5 → nonebot_plugin_dotcharacter-2.0.7}/nonebot_plugin_dotcharacter/character_loader.py +0 -0
- {nonebot_plugin_dotcharacter-2.0.5 → nonebot_plugin_dotcharacter-2.0.7}/nonebot_plugin_dotcharacter/config.py +0 -0
- {nonebot_plugin_dotcharacter-2.0.5 → nonebot_plugin_dotcharacter-2.0.7}/nonebot_plugin_dotcharacter/llm_client.py +0 -0
- {nonebot_plugin_dotcharacter-2.0.5 → nonebot_plugin_dotcharacter-2.0.7}/nonebot_plugin_dotcharacter.egg-info/SOURCES.txt +0 -0
- {nonebot_plugin_dotcharacter-2.0.5 → nonebot_plugin_dotcharacter-2.0.7}/nonebot_plugin_dotcharacter.egg-info/dependency_links.txt +0 -0
- {nonebot_plugin_dotcharacter-2.0.5 → nonebot_plugin_dotcharacter-2.0.7}/nonebot_plugin_dotcharacter.egg-info/entry_points.txt +0 -0
- {nonebot_plugin_dotcharacter-2.0.5 → nonebot_plugin_dotcharacter-2.0.7}/nonebot_plugin_dotcharacter.egg-info/top_level.txt +0 -0
- {nonebot_plugin_dotcharacter-2.0.5 → nonebot_plugin_dotcharacter-2.0.7}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nonebot-plugin-dotcharacter
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.7
|
|
4
4
|
Summary: NoneBot 插件:加载 dot-skill / colleague-skill 蒸馏角色,通过 QQ Bot 进行 AI 角色扮演对话
|
|
5
5
|
Author: tghrt
|
|
6
6
|
License: MIT
|
|
@@ -20,7 +20,6 @@ Requires-Dist: nonebot2>=2.3.0
|
|
|
20
20
|
Requires-Dist: nonebot-adapter-onebot>=0.3.0
|
|
21
21
|
Requires-Dist: httpx>=0.24.0
|
|
22
22
|
Requires-Dist: pyyaml>=6.0
|
|
23
|
-
Requires-Dist: nonebot-plugin-localstore>=0.4.0
|
|
24
23
|
Dynamic: license-file
|
|
25
24
|
|
|
26
25
|
<div align="center">
|
|
@@ -25,21 +25,6 @@ from nonebot.permission import Permission
|
|
|
25
25
|
from nonebot.plugin import PluginMetadata
|
|
26
26
|
from nonebot.rule import Rule
|
|
27
27
|
|
|
28
|
-
# 本地存储实例(NoneBot 启动后才可用)
|
|
29
|
-
_store = None
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def _get_store():
|
|
33
|
-
"""懒加载 localstore,仅在 NoneBot 运行时导入。"""
|
|
34
|
-
global _store
|
|
35
|
-
if _store is None:
|
|
36
|
-
try:
|
|
37
|
-
import nonebot_plugin_localstore as s
|
|
38
|
-
_store = s
|
|
39
|
-
except Exception:
|
|
40
|
-
pass
|
|
41
|
-
return _store
|
|
42
|
-
|
|
43
28
|
from .config import get_config, PROVIDER_PRESETS, DotCharacterConfig
|
|
44
29
|
from .character_loader import (
|
|
45
30
|
CharacterMeta,
|
|
@@ -74,7 +59,7 @@ __plugin_meta__ = PluginMetadata(
|
|
|
74
59
|
supported_adapters={"~onebot.v11"},
|
|
75
60
|
extra={
|
|
76
61
|
"author": "tghrt",
|
|
77
|
-
"version": "2.0.
|
|
62
|
+
"version": "2.0.7",
|
|
78
63
|
},
|
|
79
64
|
)
|
|
80
65
|
|
|
@@ -653,8 +638,6 @@ driver = get_driver()
|
|
|
653
638
|
@driver.on_startup
|
|
654
639
|
async def _on_startup():
|
|
655
640
|
logger.info("[dotcharacter] 插件启动中...")
|
|
656
|
-
# 懒加载 localstore(仅在 NoneBot 环境中可用)
|
|
657
|
-
_get_store()
|
|
658
641
|
await _ensure_initialized()
|
|
659
642
|
cfg = get_config()
|
|
660
643
|
admins = cfg.get_admin_qqs()
|
|
@@ -1,107 +1,184 @@
|
|
|
1
|
-
"""会话管理 — 每个用户 × 每个角色维护独立对话历史。
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def
|
|
54
|
-
return
|
|
55
|
-
|
|
56
|
-
def
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
1
|
+
"""会话管理 — 每个用户 × 每个角色维护独立对话历史。
|
|
2
|
+
|
|
3
|
+
支持持久化到本地 JSON 文件,重启后对话历史不丢失。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
from collections import OrderedDict
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
# 数据文件路径
|
|
14
|
+
_DATA_DIR = Path.home() / ".nonebot2" / "data" / "dotcharacter"
|
|
15
|
+
_SESSIONS_FILE = _DATA_DIR / "sessions.json"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ConversationSession:
|
|
20
|
+
"""单个用户对单个角色的对话会话。"""
|
|
21
|
+
user_id: str
|
|
22
|
+
character_slug: str
|
|
23
|
+
messages: List[dict] = field(default_factory=list)
|
|
24
|
+
created_at: float = field(default_factory=time.time)
|
|
25
|
+
last_active_at: float = field(default_factory=time.time)
|
|
26
|
+
_manager: Optional["ConversationManager"] = field(default=None, repr=False)
|
|
27
|
+
|
|
28
|
+
def add_user_message(self, content: str) -> None:
|
|
29
|
+
self.messages.append({"role": "user", "content": content})
|
|
30
|
+
self.last_active_at = time.time()
|
|
31
|
+
if self._manager:
|
|
32
|
+
self._manager._save()
|
|
33
|
+
|
|
34
|
+
def add_assistant_message(self, content: str) -> None:
|
|
35
|
+
self.messages.append({"role": "assistant", "content": content})
|
|
36
|
+
self.last_active_at = time.time()
|
|
37
|
+
if self._manager:
|
|
38
|
+
self._manager._save()
|
|
39
|
+
|
|
40
|
+
def trim(self, max_history: int) -> None:
|
|
41
|
+
"""保留最近 N 轮对话(每轮 = user + assistant)。"""
|
|
42
|
+
if max_history <= 0:
|
|
43
|
+
self.messages.clear()
|
|
44
|
+
if self._manager:
|
|
45
|
+
self._manager._save()
|
|
46
|
+
return
|
|
47
|
+
if len(self.messages) > max_history:
|
|
48
|
+
self.messages = self.messages[-max_history:]
|
|
49
|
+
if self._manager:
|
|
50
|
+
self._manager._save()
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def is_empty(self) -> bool:
|
|
54
|
+
return len(self.messages) == 0
|
|
55
|
+
|
|
56
|
+
def to_dict(self) -> dict:
|
|
57
|
+
return {
|
|
58
|
+
"user_id": self.user_id,
|
|
59
|
+
"character_slug": self.character_slug,
|
|
60
|
+
"messages": self.messages,
|
|
61
|
+
"created_at": self.created_at,
|
|
62
|
+
"last_active_at": self.last_active_at,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_dict(cls, data: dict, manager: Optional["ConversationManager"] = None) -> "ConversationSession":
|
|
67
|
+
return cls(
|
|
68
|
+
user_id=data["user_id"],
|
|
69
|
+
character_slug=data["character_slug"],
|
|
70
|
+
messages=data.get("messages", []),
|
|
71
|
+
created_at=data.get("created_at", time.time()),
|
|
72
|
+
last_active_at=data.get("last_active_at", time.time()),
|
|
73
|
+
_manager=manager,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ConversationManager:
|
|
78
|
+
"""管理所有用户的会话。
|
|
79
|
+
|
|
80
|
+
内部使用 OrderedDict 做简单的 LRU 淘汰,
|
|
81
|
+
支持持久化到本地 JSON 文件。
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
MAX_SESSIONS = 500 # 最多保留 500 个会话
|
|
85
|
+
|
|
86
|
+
def __init__(self) -> None:
|
|
87
|
+
self._sessions: OrderedDict[str, ConversationSession] = OrderedDict()
|
|
88
|
+
self._active_character: Dict[str, str] = {} # user_id → slug
|
|
89
|
+
self._load()
|
|
90
|
+
|
|
91
|
+
def _save(self) -> None:
|
|
92
|
+
"""将会话状态持久化到本地文件。"""
|
|
93
|
+
try:
|
|
94
|
+
_DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
data = {
|
|
96
|
+
"sessions": {
|
|
97
|
+
key: session.to_dict()
|
|
98
|
+
for key, session in self._sessions.items()
|
|
99
|
+
},
|
|
100
|
+
"active_characters": dict(self._active_character),
|
|
101
|
+
}
|
|
102
|
+
_SESSIONS_FILE.write_text(
|
|
103
|
+
json.dumps(data, ensure_ascii=False, indent=2),
|
|
104
|
+
encoding="utf-8",
|
|
105
|
+
)
|
|
106
|
+
except Exception:
|
|
107
|
+
pass # 保存失败不影响运行
|
|
108
|
+
|
|
109
|
+
def _load(self) -> None:
|
|
110
|
+
"""从本地文件恢复会话状态。"""
|
|
111
|
+
if not _SESSIONS_FILE.exists():
|
|
112
|
+
return
|
|
113
|
+
try:
|
|
114
|
+
raw = _SESSIONS_FILE.read_text(encoding="utf-8")
|
|
115
|
+
data = json.loads(raw)
|
|
116
|
+
sessions_data = data.get("sessions", {})
|
|
117
|
+
for key, sess_data in sessions_data.items():
|
|
118
|
+
session = ConversationSession.from_dict(sess_data, manager=self)
|
|
119
|
+
self._sessions[key] = session
|
|
120
|
+
self._active_character = data.get("active_characters", {})
|
|
121
|
+
except Exception:
|
|
122
|
+
pass # 加载失败不影响运行
|
|
123
|
+
|
|
124
|
+
def _make_key(self, user_id: str, slug: str) -> str:
|
|
125
|
+
return f"{user_id}::{slug}"
|
|
126
|
+
|
|
127
|
+
def get_session(self, user_id: str, slug: str) -> ConversationSession:
|
|
128
|
+
"""获取或创建会话。"""
|
|
129
|
+
key = self._make_key(user_id, slug)
|
|
130
|
+
if key not in self._sessions:
|
|
131
|
+
if len(self._sessions) >= self.MAX_SESSIONS:
|
|
132
|
+
# 淘汰最老的会话
|
|
133
|
+
self._sessions.popitem(last=False)
|
|
134
|
+
self._sessions[key] = ConversationSession(
|
|
135
|
+
user_id=user_id,
|
|
136
|
+
character_slug=slug,
|
|
137
|
+
_manager=self,
|
|
138
|
+
)
|
|
139
|
+
self._save()
|
|
140
|
+
else:
|
|
141
|
+
# 移到末尾(最近使用)
|
|
142
|
+
self._sessions.move_to_end(key)
|
|
143
|
+
return self._sessions[key]
|
|
144
|
+
|
|
145
|
+
def reset_session(self, user_id: str, slug: str) -> None:
|
|
146
|
+
"""重置某用户对某角色的对话。"""
|
|
147
|
+
key = self._make_key(user_id, slug)
|
|
148
|
+
self._sessions.pop(key, None)
|
|
149
|
+
self._save()
|
|
150
|
+
|
|
151
|
+
def set_active_character(self, user_id: str, slug: str) -> None:
|
|
152
|
+
self._active_character[user_id] = slug
|
|
153
|
+
self._save()
|
|
154
|
+
|
|
155
|
+
def get_active_character(self, user_id: str) -> Optional[str]:
|
|
156
|
+
return self._active_character.get(user_id)
|
|
157
|
+
|
|
158
|
+
def clear_active_character(self, user_id: str) -> None:
|
|
159
|
+
self._active_character.pop(user_id, None)
|
|
160
|
+
self._save()
|
|
161
|
+
|
|
162
|
+
def cleanup_stale(self, max_age_seconds: float = 3600.0) -> int:
|
|
163
|
+
"""清理超过 max_age_seconds 未活跃的会话。返回清理数量。"""
|
|
164
|
+
now = time.time()
|
|
165
|
+
stale_keys = [
|
|
166
|
+
key
|
|
167
|
+
for key, session in self._sessions.items()
|
|
168
|
+
if now - session.last_active_at > max_age_seconds
|
|
169
|
+
]
|
|
170
|
+
for key in stale_keys:
|
|
171
|
+
self._sessions.pop(key, None)
|
|
172
|
+
self._save()
|
|
173
|
+
return len(stale_keys)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# 全局单例
|
|
177
|
+
_conversation_manager: Optional[ConversationManager] = None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def get_conversation_manager() -> ConversationManager:
|
|
181
|
+
global _conversation_manager
|
|
182
|
+
if _conversation_manager is None:
|
|
183
|
+
_conversation_manager = ConversationManager()
|
|
184
|
+
return _conversation_manager
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nonebot-plugin-dotcharacter
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.7
|
|
4
4
|
Summary: NoneBot 插件:加载 dot-skill / colleague-skill 蒸馏角色,通过 QQ Bot 进行 AI 角色扮演对话
|
|
5
5
|
Author: tghrt
|
|
6
6
|
License: MIT
|
|
@@ -20,7 +20,6 @@ Requires-Dist: nonebot2>=2.3.0
|
|
|
20
20
|
Requires-Dist: nonebot-adapter-onebot>=0.3.0
|
|
21
21
|
Requires-Dist: httpx>=0.24.0
|
|
22
22
|
Requires-Dist: pyyaml>=6.0
|
|
23
|
-
Requires-Dist: nonebot-plugin-localstore>=0.4.0
|
|
24
23
|
Dynamic: license-file
|
|
25
24
|
|
|
26
25
|
<div align="center">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nonebot-plugin-dotcharacter"
|
|
3
|
-
version = "2.0.
|
|
3
|
+
version = "2.0.7"
|
|
4
4
|
description = "NoneBot 插件:加载 dot-skill / colleague-skill 蒸馏角色,通过 QQ Bot 进行 AI 角色扮演对话"
|
|
5
5
|
requires-python = ">=3.9"
|
|
6
6
|
readme = "README.md"
|
|
@@ -23,7 +23,6 @@ dependencies = [
|
|
|
23
23
|
"nonebot-adapter-onebot>=0.3.0",
|
|
24
24
|
"httpx>=0.24.0",
|
|
25
25
|
"pyyaml>=6.0",
|
|
26
|
-
"nonebot-plugin-localstore>=0.4.0",
|
|
27
26
|
]
|
|
28
27
|
|
|
29
28
|
[project.entry-points."nonebot"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|