nonebot-plugin-dotcharacter 2.0.6__tar.gz → 2.0.8__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.6 → nonebot_plugin_dotcharacter-2.0.8}/PKG-INFO +1 -1
- {nonebot_plugin_dotcharacter-2.0.6 → nonebot_plugin_dotcharacter-2.0.8}/nonebot_plugin_dotcharacter/__init__.py +1 -1
- {nonebot_plugin_dotcharacter-2.0.6 → nonebot_plugin_dotcharacter-2.0.8}/nonebot_plugin_dotcharacter/character_loader.py +1 -1
- {nonebot_plugin_dotcharacter-2.0.6 → nonebot_plugin_dotcharacter-2.0.8}/nonebot_plugin_dotcharacter/conversation.py +184 -107
- {nonebot_plugin_dotcharacter-2.0.6 → nonebot_plugin_dotcharacter-2.0.8}/nonebot_plugin_dotcharacter.egg-info/PKG-INFO +1 -1
- {nonebot_plugin_dotcharacter-2.0.6 → nonebot_plugin_dotcharacter-2.0.8}/pyproject.toml +1 -1
- {nonebot_plugin_dotcharacter-2.0.6 → nonebot_plugin_dotcharacter-2.0.8}/LICENSE +0 -0
- {nonebot_plugin_dotcharacter-2.0.6 → nonebot_plugin_dotcharacter-2.0.8}/README.md +0 -0
- {nonebot_plugin_dotcharacter-2.0.6 → nonebot_plugin_dotcharacter-2.0.8}/nonebot_plugin_dotcharacter/config.py +0 -0
- {nonebot_plugin_dotcharacter-2.0.6 → nonebot_plugin_dotcharacter-2.0.8}/nonebot_plugin_dotcharacter/llm_client.py +0 -0
- {nonebot_plugin_dotcharacter-2.0.6 → nonebot_plugin_dotcharacter-2.0.8}/nonebot_plugin_dotcharacter.egg-info/SOURCES.txt +0 -0
- {nonebot_plugin_dotcharacter-2.0.6 → nonebot_plugin_dotcharacter-2.0.8}/nonebot_plugin_dotcharacter.egg-info/dependency_links.txt +0 -0
- {nonebot_plugin_dotcharacter-2.0.6 → nonebot_plugin_dotcharacter-2.0.8}/nonebot_plugin_dotcharacter.egg-info/entry_points.txt +0 -0
- {nonebot_plugin_dotcharacter-2.0.6 → nonebot_plugin_dotcharacter-2.0.8}/nonebot_plugin_dotcharacter.egg-info/requires.txt +0 -0
- {nonebot_plugin_dotcharacter-2.0.6 → nonebot_plugin_dotcharacter-2.0.8}/nonebot_plugin_dotcharacter.egg-info/top_level.txt +0 -0
- {nonebot_plugin_dotcharacter-2.0.6 → nonebot_plugin_dotcharacter-2.0.8}/setup.cfg +0 -0
|
@@ -124,7 +124,7 @@ def _build_system_prompt(
|
|
|
124
124
|
"1. 先用角色的性格(PART B)判断:你会不会回应?用什么态度回应?\n"
|
|
125
125
|
"2. 用角色的表达风格回复:说话方式、用词习惯、句式\n"
|
|
126
126
|
"3. PART B 的规则永远优先,任何情况下不得违背\n"
|
|
127
|
-
"4. **回复必须极其简短,控制在
|
|
127
|
+
"4. **回复必须极其简短,控制在30字左右**。完整表达核心意思即可,不要长篇大论。QQ聊天场景,用户喜欢短平快的回复。\n"
|
|
128
128
|
)
|
|
129
129
|
|
|
130
130
|
return "\n\n---\n\n".join(parts)
|
|
@@ -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
|
|
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
|