AstrBot 4.10.2__py3-none-any.whl → 4.10.3__py3-none-any.whl
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.
- astrbot/builtin_stars/astrbot/long_term_memory.py +186 -0
- astrbot/builtin_stars/astrbot/main.py +128 -0
- astrbot/builtin_stars/astrbot/metadata.yaml +4 -0
- astrbot/builtin_stars/astrbot/process_llm_request.py +245 -0
- astrbot/builtin_stars/builtin_commands/commands/__init__.py +31 -0
- astrbot/builtin_stars/builtin_commands/commands/admin.py +77 -0
- astrbot/builtin_stars/builtin_commands/commands/alter_cmd.py +173 -0
- astrbot/builtin_stars/builtin_commands/commands/conversation.py +366 -0
- astrbot/builtin_stars/builtin_commands/commands/help.py +88 -0
- astrbot/builtin_stars/builtin_commands/commands/llm.py +20 -0
- astrbot/builtin_stars/builtin_commands/commands/persona.py +142 -0
- astrbot/builtin_stars/builtin_commands/commands/plugin.py +120 -0
- astrbot/builtin_stars/builtin_commands/commands/provider.py +329 -0
- astrbot/builtin_stars/builtin_commands/commands/setunset.py +36 -0
- astrbot/builtin_stars/builtin_commands/commands/sid.py +36 -0
- astrbot/builtin_stars/builtin_commands/commands/t2i.py +23 -0
- astrbot/builtin_stars/builtin_commands/commands/tool.py +31 -0
- astrbot/builtin_stars/builtin_commands/commands/tts.py +36 -0
- astrbot/builtin_stars/builtin_commands/commands/utils/rst_scene.py +26 -0
- astrbot/builtin_stars/builtin_commands/main.py +237 -0
- astrbot/builtin_stars/builtin_commands/metadata.yaml +4 -0
- astrbot/builtin_stars/python_interpreter/main.py +537 -0
- astrbot/builtin_stars/python_interpreter/metadata.yaml +4 -0
- astrbot/builtin_stars/python_interpreter/requirements.txt +1 -0
- astrbot/builtin_stars/python_interpreter/shared/api.py +22 -0
- astrbot/builtin_stars/reminder/main.py +266 -0
- astrbot/builtin_stars/reminder/metadata.yaml +4 -0
- astrbot/builtin_stars/session_controller/main.py +114 -0
- astrbot/builtin_stars/session_controller/metadata.yaml +5 -0
- astrbot/builtin_stars/web_searcher/engines/__init__.py +111 -0
- astrbot/builtin_stars/web_searcher/engines/bing.py +30 -0
- astrbot/builtin_stars/web_searcher/engines/sogo.py +52 -0
- astrbot/builtin_stars/web_searcher/main.py +436 -0
- astrbot/builtin_stars/web_searcher/metadata.yaml +4 -0
- astrbot/cli/__init__.py +1 -1
- astrbot/core/agent/message.py +9 -0
- astrbot/core/agent/runners/tool_loop_agent_runner.py +2 -1
- astrbot/core/backup/__init__.py +26 -0
- astrbot/core/backup/constants.py +77 -0
- astrbot/core/backup/exporter.py +476 -0
- astrbot/core/backup/importer.py +761 -0
- astrbot/core/config/default.py +1 -1
- astrbot/core/log.py +1 -1
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +1 -1
- astrbot/core/pipeline/waking_check/stage.py +2 -1
- astrbot/core/provider/entities.py +32 -9
- astrbot/core/provider/provider.py +3 -1
- astrbot/core/provider/sources/anthropic_source.py +80 -27
- astrbot/core/provider/sources/fishaudio_tts_api_source.py +14 -6
- astrbot/core/provider/sources/gemini_source.py +75 -26
- astrbot/core/provider/sources/openai_source.py +68 -25
- astrbot/core/star/context.py +1 -1
- astrbot/core/star/star_manager.py +11 -13
- astrbot/core/utils/astrbot_path.py +34 -0
- astrbot/dashboard/routes/__init__.py +2 -0
- astrbot/dashboard/routes/backup.py +589 -0
- astrbot/dashboard/routes/log.py +44 -10
- astrbot/dashboard/server.py +8 -1
- {astrbot-4.10.2.dist-info → astrbot-4.10.3.dist-info}/METADATA +1 -1
- {astrbot-4.10.2.dist-info → astrbot-4.10.3.dist-info}/RECORD +63 -24
- {astrbot-4.10.2.dist-info → astrbot-4.10.3.dist-info}/WHEEL +0 -0
- {astrbot-4.10.2.dist-info → astrbot-4.10.3.dist-info}/entry_points.txt +0 -0
- {astrbot-4.10.2.dist-info → astrbot-4.10.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import random
|
|
3
|
+
import uuid
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
|
|
6
|
+
from astrbot import logger
|
|
7
|
+
from astrbot.api import star
|
|
8
|
+
from astrbot.api.event import AstrMessageEvent
|
|
9
|
+
from astrbot.api.message_components import At, Image, Plain
|
|
10
|
+
from astrbot.api.platform import MessageType
|
|
11
|
+
from astrbot.api.provider import LLMResponse, Provider, ProviderRequest
|
|
12
|
+
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
聊天记忆增强
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LongTermMemory:
|
|
20
|
+
def __init__(self, acm: AstrBotConfigManager, context: star.Context):
|
|
21
|
+
self.acm = acm
|
|
22
|
+
self.context = context
|
|
23
|
+
self.session_chats = defaultdict(list)
|
|
24
|
+
"""记录群成员的群聊记录"""
|
|
25
|
+
|
|
26
|
+
def cfg(self, event: AstrMessageEvent):
|
|
27
|
+
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
|
28
|
+
try:
|
|
29
|
+
max_cnt = int(cfg["provider_ltm_settings"]["group_message_max_cnt"])
|
|
30
|
+
except BaseException as e:
|
|
31
|
+
logger.error(e)
|
|
32
|
+
max_cnt = 300
|
|
33
|
+
image_caption_prompt = cfg["provider_settings"]["image_caption_prompt"]
|
|
34
|
+
image_caption_provider_id = cfg["provider_ltm_settings"].get(
|
|
35
|
+
"image_caption_provider_id"
|
|
36
|
+
)
|
|
37
|
+
image_caption = cfg["provider_ltm_settings"]["image_caption"] and bool(
|
|
38
|
+
image_caption_provider_id
|
|
39
|
+
)
|
|
40
|
+
active_reply = cfg["provider_ltm_settings"]["active_reply"]
|
|
41
|
+
enable_active_reply = active_reply.get("enable", False)
|
|
42
|
+
ar_method = active_reply["method"]
|
|
43
|
+
ar_possibility = active_reply["possibility_reply"]
|
|
44
|
+
ar_prompt = active_reply.get("prompt", "")
|
|
45
|
+
ar_whitelist = active_reply.get("whitelist", [])
|
|
46
|
+
ret = {
|
|
47
|
+
"max_cnt": max_cnt,
|
|
48
|
+
"image_caption": image_caption,
|
|
49
|
+
"image_caption_prompt": image_caption_prompt,
|
|
50
|
+
"image_caption_provider_id": image_caption_provider_id,
|
|
51
|
+
"enable_active_reply": enable_active_reply,
|
|
52
|
+
"ar_method": ar_method,
|
|
53
|
+
"ar_possibility": ar_possibility,
|
|
54
|
+
"ar_prompt": ar_prompt,
|
|
55
|
+
"ar_whitelist": ar_whitelist,
|
|
56
|
+
}
|
|
57
|
+
return ret
|
|
58
|
+
|
|
59
|
+
async def remove_session(self, event: AstrMessageEvent) -> int:
|
|
60
|
+
cnt = 0
|
|
61
|
+
if event.unified_msg_origin in self.session_chats:
|
|
62
|
+
cnt = len(self.session_chats[event.unified_msg_origin])
|
|
63
|
+
del self.session_chats[event.unified_msg_origin]
|
|
64
|
+
return cnt
|
|
65
|
+
|
|
66
|
+
async def get_image_caption(
|
|
67
|
+
self,
|
|
68
|
+
image_url: str,
|
|
69
|
+
image_caption_provider_id: str,
|
|
70
|
+
image_caption_prompt: str,
|
|
71
|
+
) -> str:
|
|
72
|
+
if not image_caption_provider_id:
|
|
73
|
+
provider = self.context.get_using_provider()
|
|
74
|
+
else:
|
|
75
|
+
provider = self.context.get_provider_by_id(image_caption_provider_id)
|
|
76
|
+
if not provider:
|
|
77
|
+
raise Exception(f"没有找到 ID 为 {image_caption_provider_id} 的提供商")
|
|
78
|
+
if not isinstance(provider, Provider):
|
|
79
|
+
raise Exception(f"提供商类型错误({type(provider)}),无法获取图片描述")
|
|
80
|
+
response = await provider.text_chat(
|
|
81
|
+
prompt=image_caption_prompt,
|
|
82
|
+
session_id=uuid.uuid4().hex,
|
|
83
|
+
image_urls=[image_url],
|
|
84
|
+
persist=False,
|
|
85
|
+
)
|
|
86
|
+
return response.completion_text
|
|
87
|
+
|
|
88
|
+
async def need_active_reply(self, event: AstrMessageEvent) -> bool:
|
|
89
|
+
cfg = self.cfg(event)
|
|
90
|
+
if not cfg["enable_active_reply"]:
|
|
91
|
+
return False
|
|
92
|
+
if event.get_message_type() != MessageType.GROUP_MESSAGE:
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
if event.is_at_or_wake_command:
|
|
96
|
+
# if the message is a command, let it pass
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
if cfg["ar_whitelist"] and (
|
|
100
|
+
event.unified_msg_origin not in cfg["ar_whitelist"]
|
|
101
|
+
and (
|
|
102
|
+
event.get_group_id() and event.get_group_id() not in cfg["ar_whitelist"]
|
|
103
|
+
)
|
|
104
|
+
):
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
match cfg["ar_method"]:
|
|
108
|
+
case "possibility_reply":
|
|
109
|
+
trig = random.random() < cfg["ar_possibility"]
|
|
110
|
+
return trig
|
|
111
|
+
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
async def handle_message(self, event: AstrMessageEvent):
|
|
115
|
+
"""仅支持群聊"""
|
|
116
|
+
if event.get_message_type() == MessageType.GROUP_MESSAGE:
|
|
117
|
+
datetime_str = datetime.datetime.now().strftime("%H:%M:%S")
|
|
118
|
+
|
|
119
|
+
parts = [f"[{event.message_obj.sender.nickname}/{datetime_str}]: "]
|
|
120
|
+
|
|
121
|
+
cfg = self.cfg(event)
|
|
122
|
+
|
|
123
|
+
for comp in event.get_messages():
|
|
124
|
+
if isinstance(comp, Plain):
|
|
125
|
+
parts.append(f" {comp.text}")
|
|
126
|
+
elif isinstance(comp, Image):
|
|
127
|
+
if cfg["image_caption"]:
|
|
128
|
+
try:
|
|
129
|
+
url = comp.url if comp.url else comp.file
|
|
130
|
+
if not url:
|
|
131
|
+
raise Exception("图片 URL 为空")
|
|
132
|
+
caption = await self.get_image_caption(
|
|
133
|
+
url,
|
|
134
|
+
cfg["image_caption_provider_id"],
|
|
135
|
+
cfg["image_caption_prompt"],
|
|
136
|
+
)
|
|
137
|
+
parts.append(f" [Image: {caption}]")
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.error(f"获取图片描述失败: {e}")
|
|
140
|
+
else:
|
|
141
|
+
parts.append(" [Image]")
|
|
142
|
+
elif isinstance(comp, At):
|
|
143
|
+
parts.append(f" [At: {comp.name}]")
|
|
144
|
+
|
|
145
|
+
final_message = "".join(parts)
|
|
146
|
+
logger.debug(f"ltm | {event.unified_msg_origin} | {final_message}")
|
|
147
|
+
self.session_chats[event.unified_msg_origin].append(final_message)
|
|
148
|
+
if len(self.session_chats[event.unified_msg_origin]) > cfg["max_cnt"]:
|
|
149
|
+
self.session_chats[event.unified_msg_origin].pop(0)
|
|
150
|
+
|
|
151
|
+
async def on_req_llm(self, event: AstrMessageEvent, req: ProviderRequest):
|
|
152
|
+
"""当触发 LLM 请求前,调用此方法修改 req"""
|
|
153
|
+
if event.unified_msg_origin not in self.session_chats:
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
chats_str = "\n---\n".join(self.session_chats[event.unified_msg_origin])
|
|
157
|
+
|
|
158
|
+
cfg = self.cfg(event)
|
|
159
|
+
if cfg["enable_active_reply"]:
|
|
160
|
+
prompt = req.prompt
|
|
161
|
+
req.prompt = (
|
|
162
|
+
f"You are now in a chatroom. The chat history is as follows:\n{chats_str}"
|
|
163
|
+
f"\nNow, a new message is coming: `{prompt}`. "
|
|
164
|
+
"Please react to it. Only output your response and do not output any other information. "
|
|
165
|
+
"You MUST use the SAME language as the chatroom is using."
|
|
166
|
+
)
|
|
167
|
+
req.contexts = [] # 清空上下文,当使用了主动回复,所有聊天记录都在一个prompt中。
|
|
168
|
+
else:
|
|
169
|
+
req.system_prompt += (
|
|
170
|
+
"You are now in a chatroom. The chat history is as follows: \n"
|
|
171
|
+
)
|
|
172
|
+
req.system_prompt += chats_str
|
|
173
|
+
|
|
174
|
+
async def after_req_llm(self, event: AstrMessageEvent, llm_resp: LLMResponse):
|
|
175
|
+
if event.unified_msg_origin not in self.session_chats:
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
if llm_resp.completion_text:
|
|
179
|
+
final_message = f"[You/{datetime.datetime.now().strftime('%H:%M:%S')}]: {llm_resp.completion_text}"
|
|
180
|
+
logger.debug(
|
|
181
|
+
f"Recorded AI response: {event.unified_msg_origin} | {final_message}"
|
|
182
|
+
)
|
|
183
|
+
self.session_chats[event.unified_msg_origin].append(final_message)
|
|
184
|
+
cfg = self.cfg(event)
|
|
185
|
+
if len(self.session_chats[event.unified_msg_origin]) > cfg["max_cnt"]:
|
|
186
|
+
self.session_chats[event.unified_msg_origin].pop(0)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
|
|
3
|
+
from astrbot.api import star
|
|
4
|
+
from astrbot.api.event import AstrMessageEvent, filter
|
|
5
|
+
from astrbot.api.message_components import Image, Plain
|
|
6
|
+
from astrbot.api.provider import LLMResponse, ProviderRequest
|
|
7
|
+
from astrbot.core import logger
|
|
8
|
+
|
|
9
|
+
from .long_term_memory import LongTermMemory
|
|
10
|
+
from .process_llm_request import ProcessLLMRequest
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Main(star.Star):
|
|
14
|
+
def __init__(self, context: star.Context) -> None:
|
|
15
|
+
self.context = context
|
|
16
|
+
self.ltm = None
|
|
17
|
+
try:
|
|
18
|
+
self.ltm = LongTermMemory(self.context.astrbot_config_mgr, self.context)
|
|
19
|
+
except BaseException as e:
|
|
20
|
+
logger.error(f"聊天增强 err: {e}")
|
|
21
|
+
|
|
22
|
+
self.proc_llm_req = ProcessLLMRequest(self.context)
|
|
23
|
+
|
|
24
|
+
def ltm_enabled(self, event: AstrMessageEvent):
|
|
25
|
+
ltmse = self.context.get_config(umo=event.unified_msg_origin)[
|
|
26
|
+
"provider_ltm_settings"
|
|
27
|
+
]
|
|
28
|
+
return ltmse["group_icl_enable"] or ltmse["active_reply"]["enable"]
|
|
29
|
+
|
|
30
|
+
@filter.platform_adapter_type(filter.PlatformAdapterType.ALL)
|
|
31
|
+
async def on_message(self, event: AstrMessageEvent):
|
|
32
|
+
"""群聊记忆增强"""
|
|
33
|
+
has_image_or_plain = False
|
|
34
|
+
for comp in event.message_obj.message:
|
|
35
|
+
if isinstance(comp, Plain) or isinstance(comp, Image):
|
|
36
|
+
has_image_or_plain = True
|
|
37
|
+
break
|
|
38
|
+
|
|
39
|
+
if self.ltm_enabled(event) and self.ltm and has_image_or_plain:
|
|
40
|
+
need_active = await self.ltm.need_active_reply(event)
|
|
41
|
+
|
|
42
|
+
group_icl_enable = self.context.get_config()["provider_ltm_settings"][
|
|
43
|
+
"group_icl_enable"
|
|
44
|
+
]
|
|
45
|
+
if group_icl_enable:
|
|
46
|
+
"""记录对话"""
|
|
47
|
+
try:
|
|
48
|
+
await self.ltm.handle_message(event)
|
|
49
|
+
except BaseException as e:
|
|
50
|
+
logger.error(e)
|
|
51
|
+
|
|
52
|
+
if need_active:
|
|
53
|
+
"""主动回复"""
|
|
54
|
+
provider = self.context.get_using_provider(event.unified_msg_origin)
|
|
55
|
+
if not provider:
|
|
56
|
+
logger.error("未找到任何 LLM 提供商。请先配置。无法主动回复")
|
|
57
|
+
return
|
|
58
|
+
try:
|
|
59
|
+
conv = None
|
|
60
|
+
session_curr_cid = await self.context.conversation_manager.get_curr_conversation_id(
|
|
61
|
+
event.unified_msg_origin,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if not session_curr_cid:
|
|
65
|
+
logger.error(
|
|
66
|
+
"当前未处于对话状态,无法主动回复,请确保 平台设置->会话隔离(unique_session) 未开启,并使用 /switch 序号 切换或者 /new 创建一个会话。",
|
|
67
|
+
)
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
conv = await self.context.conversation_manager.get_conversation(
|
|
71
|
+
event.unified_msg_origin,
|
|
72
|
+
session_curr_cid,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
prompt = event.message_str
|
|
76
|
+
|
|
77
|
+
if not conv:
|
|
78
|
+
logger.error("未找到对话,无法主动回复")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
yield event.request_llm(
|
|
82
|
+
prompt=prompt,
|
|
83
|
+
func_tool_manager=self.context.get_llm_tool_manager(),
|
|
84
|
+
session_id=event.session_id,
|
|
85
|
+
conversation=conv,
|
|
86
|
+
)
|
|
87
|
+
except BaseException as e:
|
|
88
|
+
logger.error(traceback.format_exc())
|
|
89
|
+
logger.error(f"主动回复失败: {e}")
|
|
90
|
+
|
|
91
|
+
@filter.on_llm_request()
|
|
92
|
+
async def decorate_llm_req(self, event: AstrMessageEvent, req: ProviderRequest):
|
|
93
|
+
"""在请求 LLM 前注入人格信息、Identifier、时间、回复内容等 System Prompt"""
|
|
94
|
+
await self.proc_llm_req.process_llm_request(event, req)
|
|
95
|
+
|
|
96
|
+
if self.ltm and self.ltm_enabled(event):
|
|
97
|
+
try:
|
|
98
|
+
await self.ltm.on_req_llm(event, req)
|
|
99
|
+
except BaseException as e:
|
|
100
|
+
logger.error(f"ltm: {e}")
|
|
101
|
+
|
|
102
|
+
@filter.on_llm_response()
|
|
103
|
+
async def inject_reasoning(self, event: AstrMessageEvent, resp: LLMResponse):
|
|
104
|
+
"""在 LLM 响应后基于配置注入思考过程文本 / 在 LLM 响应后记录对话"""
|
|
105
|
+
umo = event.unified_msg_origin
|
|
106
|
+
cfg = self.context.get_config(umo).get("provider_settings", {})
|
|
107
|
+
show_reasoning = cfg.get("display_reasoning_text", False)
|
|
108
|
+
if show_reasoning and resp.reasoning_content:
|
|
109
|
+
resp.completion_text = (
|
|
110
|
+
f"🤔 思考: {resp.reasoning_content}\n\n{resp.completion_text}"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if self.ltm and self.ltm_enabled(event):
|
|
114
|
+
try:
|
|
115
|
+
await self.ltm.after_req_llm(event, resp)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.error(f"ltm: {e}")
|
|
118
|
+
|
|
119
|
+
@filter.after_message_sent()
|
|
120
|
+
async def after_message_sent(self, event: AstrMessageEvent):
|
|
121
|
+
"""消息发送后处理"""
|
|
122
|
+
if self.ltm and self.ltm_enabled(event):
|
|
123
|
+
try:
|
|
124
|
+
clean_session = event.get_extra("_clean_ltm_session", False)
|
|
125
|
+
if clean_session:
|
|
126
|
+
await self.ltm.remove_session(event)
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.error(f"ltm: {e}")
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import builtins
|
|
2
|
+
import copy
|
|
3
|
+
import datetime
|
|
4
|
+
import zoneinfo
|
|
5
|
+
|
|
6
|
+
from astrbot.api import logger, sp, star
|
|
7
|
+
from astrbot.api.event import AstrMessageEvent
|
|
8
|
+
from astrbot.api.message_components import Image, Reply
|
|
9
|
+
from astrbot.api.provider import Provider, ProviderRequest
|
|
10
|
+
from astrbot.core.agent.message import TextPart
|
|
11
|
+
from astrbot.core.provider.func_tool_manager import ToolSet
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProcessLLMRequest:
|
|
15
|
+
def __init__(self, context: star.Context):
|
|
16
|
+
self.ctx = context
|
|
17
|
+
cfg = context.get_config()
|
|
18
|
+
self.timezone = cfg.get("timezone")
|
|
19
|
+
if not self.timezone:
|
|
20
|
+
# 系统默认时区
|
|
21
|
+
self.timezone = None
|
|
22
|
+
else:
|
|
23
|
+
logger.info(f"Timezone set to: {self.timezone}")
|
|
24
|
+
|
|
25
|
+
async def _ensure_persona(self, req: ProviderRequest, cfg: dict, umo: str):
|
|
26
|
+
"""确保用户人格已加载"""
|
|
27
|
+
if not req.conversation:
|
|
28
|
+
return
|
|
29
|
+
# persona inject
|
|
30
|
+
|
|
31
|
+
# custom rule is preferred
|
|
32
|
+
persona_id = (
|
|
33
|
+
await sp.get_async(
|
|
34
|
+
scope="umo", scope_id=umo, key="session_service_config", default={}
|
|
35
|
+
)
|
|
36
|
+
).get("persona_id")
|
|
37
|
+
|
|
38
|
+
if not persona_id:
|
|
39
|
+
persona_id = req.conversation.persona_id or cfg.get("default_personality")
|
|
40
|
+
if not persona_id and persona_id != "[%None]": # [%None] 为用户取消人格
|
|
41
|
+
default_persona = self.ctx.persona_manager.selected_default_persona_v3
|
|
42
|
+
if default_persona:
|
|
43
|
+
persona_id = default_persona["name"]
|
|
44
|
+
|
|
45
|
+
persona = next(
|
|
46
|
+
builtins.filter(
|
|
47
|
+
lambda persona: persona["name"] == persona_id,
|
|
48
|
+
self.ctx.persona_manager.personas_v3,
|
|
49
|
+
),
|
|
50
|
+
None,
|
|
51
|
+
)
|
|
52
|
+
if persona:
|
|
53
|
+
if prompt := persona["prompt"]:
|
|
54
|
+
req.system_prompt += prompt
|
|
55
|
+
if begin_dialogs := copy.deepcopy(persona["_begin_dialogs_processed"]):
|
|
56
|
+
req.contexts[:0] = begin_dialogs
|
|
57
|
+
|
|
58
|
+
# tools select
|
|
59
|
+
tmgr = self.ctx.get_llm_tool_manager()
|
|
60
|
+
if (persona and persona.get("tools") is None) or not persona:
|
|
61
|
+
# select all
|
|
62
|
+
toolset = tmgr.get_full_tool_set()
|
|
63
|
+
for tool in toolset:
|
|
64
|
+
if not tool.active:
|
|
65
|
+
toolset.remove_tool(tool.name)
|
|
66
|
+
else:
|
|
67
|
+
toolset = ToolSet()
|
|
68
|
+
if persona["tools"]:
|
|
69
|
+
for tool_name in persona["tools"]:
|
|
70
|
+
tool = tmgr.get_func(tool_name)
|
|
71
|
+
if tool and tool.active:
|
|
72
|
+
toolset.add_tool(tool)
|
|
73
|
+
req.func_tool = toolset
|
|
74
|
+
logger.debug(f"Tool set for persona {persona_id}: {toolset.names()}")
|
|
75
|
+
|
|
76
|
+
async def _ensure_img_caption(
|
|
77
|
+
self,
|
|
78
|
+
req: ProviderRequest,
|
|
79
|
+
cfg: dict,
|
|
80
|
+
img_cap_prov_id: str,
|
|
81
|
+
):
|
|
82
|
+
try:
|
|
83
|
+
caption = await self._request_img_caption(
|
|
84
|
+
img_cap_prov_id,
|
|
85
|
+
cfg,
|
|
86
|
+
req.image_urls,
|
|
87
|
+
)
|
|
88
|
+
if caption:
|
|
89
|
+
req.extra_user_content_parts.append(
|
|
90
|
+
TextPart(text=f"<image_caption>{caption}</image_caption>")
|
|
91
|
+
)
|
|
92
|
+
req.image_urls = []
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.error(f"处理图片描述失败: {e}")
|
|
95
|
+
|
|
96
|
+
async def _request_img_caption(
|
|
97
|
+
self,
|
|
98
|
+
provider_id: str,
|
|
99
|
+
cfg: dict,
|
|
100
|
+
image_urls: list[str],
|
|
101
|
+
) -> str:
|
|
102
|
+
if prov := self.ctx.get_provider_by_id(provider_id):
|
|
103
|
+
if isinstance(prov, Provider):
|
|
104
|
+
img_cap_prompt = cfg.get(
|
|
105
|
+
"image_caption_prompt",
|
|
106
|
+
"Please describe the image.",
|
|
107
|
+
)
|
|
108
|
+
logger.debug(f"Processing image caption with provider: {provider_id}")
|
|
109
|
+
llm_resp = await prov.text_chat(
|
|
110
|
+
prompt=img_cap_prompt,
|
|
111
|
+
image_urls=image_urls,
|
|
112
|
+
)
|
|
113
|
+
return llm_resp.completion_text
|
|
114
|
+
raise ValueError(
|
|
115
|
+
f"Cannot get image caption because provider `{provider_id}` is not a valid Provider, it is {type(prov)}.",
|
|
116
|
+
)
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"Cannot get image caption because provider `{provider_id}` is not exist.",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
async def process_llm_request(self, event: AstrMessageEvent, req: ProviderRequest):
|
|
122
|
+
"""在请求 LLM 前注入人格信息、Identifier、时间、回复内容等 System Prompt"""
|
|
123
|
+
cfg: dict = self.ctx.get_config(umo=event.unified_msg_origin)[
|
|
124
|
+
"provider_settings"
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
# prompt prefix
|
|
128
|
+
if prefix := cfg.get("prompt_prefix"):
|
|
129
|
+
# 支持 {{prompt}} 作为用户输入的占位符
|
|
130
|
+
if "{{prompt}}" in prefix:
|
|
131
|
+
req.prompt = prefix.replace("{{prompt}}", req.prompt)
|
|
132
|
+
else:
|
|
133
|
+
req.prompt = prefix + req.prompt
|
|
134
|
+
|
|
135
|
+
# 收集系统提醒信息
|
|
136
|
+
system_parts = []
|
|
137
|
+
|
|
138
|
+
# user identifier
|
|
139
|
+
if cfg.get("identifier"):
|
|
140
|
+
user_id = event.message_obj.sender.user_id
|
|
141
|
+
user_nickname = event.message_obj.sender.nickname
|
|
142
|
+
system_parts.append(f"User ID: {user_id}, Nickname: {user_nickname}")
|
|
143
|
+
|
|
144
|
+
# group name identifier
|
|
145
|
+
if cfg.get("group_name_display") and event.message_obj.group_id:
|
|
146
|
+
if not event.message_obj.group:
|
|
147
|
+
logger.error(
|
|
148
|
+
f"Group name display enabled but group object is None. Group ID: {event.message_obj.group_id}"
|
|
149
|
+
)
|
|
150
|
+
return
|
|
151
|
+
group_name = event.message_obj.group.group_name
|
|
152
|
+
if group_name:
|
|
153
|
+
system_parts.append(f"Group name: {group_name}")
|
|
154
|
+
|
|
155
|
+
# time info
|
|
156
|
+
if cfg.get("datetime_system_prompt"):
|
|
157
|
+
current_time = None
|
|
158
|
+
if self.timezone:
|
|
159
|
+
# 启用时区
|
|
160
|
+
try:
|
|
161
|
+
now = datetime.datetime.now(zoneinfo.ZoneInfo(self.timezone))
|
|
162
|
+
current_time = now.strftime("%Y-%m-%d %H:%M (%Z)")
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.error(f"时区设置错误: {e}, 使用本地时区")
|
|
165
|
+
if not current_time:
|
|
166
|
+
current_time = (
|
|
167
|
+
datetime.datetime.now().astimezone().strftime("%Y-%m-%d %H:%M (%Z)")
|
|
168
|
+
)
|
|
169
|
+
system_parts.append(f"Current datetime: {current_time}")
|
|
170
|
+
|
|
171
|
+
img_cap_prov_id: str = cfg.get("default_image_caption_provider_id") or ""
|
|
172
|
+
if req.conversation:
|
|
173
|
+
# inject persona for this request
|
|
174
|
+
await self._ensure_persona(req, cfg, event.unified_msg_origin)
|
|
175
|
+
|
|
176
|
+
# image caption
|
|
177
|
+
if img_cap_prov_id and req.image_urls:
|
|
178
|
+
await self._ensure_img_caption(req, cfg, img_cap_prov_id)
|
|
179
|
+
|
|
180
|
+
# quote message processing
|
|
181
|
+
# 解析引用内容
|
|
182
|
+
quote = None
|
|
183
|
+
for comp in event.message_obj.message:
|
|
184
|
+
if isinstance(comp, Reply):
|
|
185
|
+
quote = comp
|
|
186
|
+
break
|
|
187
|
+
if quote:
|
|
188
|
+
content_parts = []
|
|
189
|
+
|
|
190
|
+
# 1. 处理引用的文本
|
|
191
|
+
sender_info = (
|
|
192
|
+
f"({quote.sender_nickname}): " if quote.sender_nickname else ""
|
|
193
|
+
)
|
|
194
|
+
message_str = quote.message_str or "[Empty Text]"
|
|
195
|
+
content_parts.append(f"{sender_info}{message_str}")
|
|
196
|
+
|
|
197
|
+
# 2. 处理引用的图片 (保留原有逻辑,但改变输出目标)
|
|
198
|
+
image_seg = None
|
|
199
|
+
if quote.chain:
|
|
200
|
+
for comp in quote.chain:
|
|
201
|
+
if isinstance(comp, Image):
|
|
202
|
+
image_seg = comp
|
|
203
|
+
break
|
|
204
|
+
|
|
205
|
+
if image_seg:
|
|
206
|
+
try:
|
|
207
|
+
# 找到可以生成图片描述的 provider
|
|
208
|
+
prov = None
|
|
209
|
+
if img_cap_prov_id:
|
|
210
|
+
prov = self.ctx.get_provider_by_id(img_cap_prov_id)
|
|
211
|
+
if prov is None:
|
|
212
|
+
prov = self.ctx.get_using_provider(event.unified_msg_origin)
|
|
213
|
+
|
|
214
|
+
# 调用 provider 生成图片描述
|
|
215
|
+
if prov and isinstance(prov, Provider):
|
|
216
|
+
llm_resp = await prov.text_chat(
|
|
217
|
+
prompt="Please describe the image content.",
|
|
218
|
+
image_urls=[await image_seg.convert_to_file_path()],
|
|
219
|
+
)
|
|
220
|
+
if llm_resp.completion_text:
|
|
221
|
+
# 将图片描述作为文本添加到 content_parts
|
|
222
|
+
content_parts.append(
|
|
223
|
+
f"[Image Caption in quoted message]: {llm_resp.completion_text}"
|
|
224
|
+
)
|
|
225
|
+
else:
|
|
226
|
+
logger.warning(
|
|
227
|
+
"No provider found for image captioning in quote."
|
|
228
|
+
)
|
|
229
|
+
except BaseException as e:
|
|
230
|
+
logger.error(f"处理引用图片失败: {e}")
|
|
231
|
+
|
|
232
|
+
# 3. 将所有部分组合成文本并添加到 extra_user_content_parts 中
|
|
233
|
+
# 确保引用内容被正确的标签包裹
|
|
234
|
+
quoted_content = "\n".join(content_parts)
|
|
235
|
+
# 确保所有内容都在<Quoted Message>标签内
|
|
236
|
+
quoted_text = f"<Quoted Message>\n{quoted_content}\n</Quoted Message>"
|
|
237
|
+
|
|
238
|
+
req.extra_user_content_parts.append(TextPart(text=quoted_text))
|
|
239
|
+
|
|
240
|
+
# 统一包裹所有系统提醒
|
|
241
|
+
if system_parts:
|
|
242
|
+
system_content = (
|
|
243
|
+
"<system_reminder>" + "\n".join(system_parts) + "</system_reminder>"
|
|
244
|
+
)
|
|
245
|
+
req.extra_user_content_parts.append(TextPart(text=system_content))
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Commands module
|
|
2
|
+
|
|
3
|
+
from .admin import AdminCommands
|
|
4
|
+
from .alter_cmd import AlterCmdCommands
|
|
5
|
+
from .conversation import ConversationCommands
|
|
6
|
+
from .help import HelpCommand
|
|
7
|
+
from .llm import LLMCommands
|
|
8
|
+
from .persona import PersonaCommands
|
|
9
|
+
from .plugin import PluginCommands
|
|
10
|
+
from .provider import ProviderCommands
|
|
11
|
+
from .setunset import SetUnsetCommands
|
|
12
|
+
from .sid import SIDCommand
|
|
13
|
+
from .t2i import T2ICommand
|
|
14
|
+
from .tool import ToolCommands
|
|
15
|
+
from .tts import TTSCommand
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"AdminCommands",
|
|
19
|
+
"AlterCmdCommands",
|
|
20
|
+
"ConversationCommands",
|
|
21
|
+
"HelpCommand",
|
|
22
|
+
"LLMCommands",
|
|
23
|
+
"PersonaCommands",
|
|
24
|
+
"PluginCommands",
|
|
25
|
+
"ProviderCommands",
|
|
26
|
+
"SIDCommand",
|
|
27
|
+
"SetUnsetCommands",
|
|
28
|
+
"T2ICommand",
|
|
29
|
+
"TTSCommand",
|
|
30
|
+
"ToolCommands",
|
|
31
|
+
]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from astrbot.api import star
|
|
2
|
+
from astrbot.api.event import AstrMessageEvent, MessageChain, MessageEventResult
|
|
3
|
+
from astrbot.core.config.default import VERSION
|
|
4
|
+
from astrbot.core.utils.io import download_dashboard
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AdminCommands:
|
|
8
|
+
def __init__(self, context: star.Context):
|
|
9
|
+
self.context = context
|
|
10
|
+
|
|
11
|
+
async def op(self, event: AstrMessageEvent, admin_id: str = ""):
|
|
12
|
+
"""授权管理员。op <admin_id>"""
|
|
13
|
+
if not admin_id:
|
|
14
|
+
event.set_result(
|
|
15
|
+
MessageEventResult().message(
|
|
16
|
+
"使用方法: /op <id> 授权管理员;/deop <id> 取消管理员。可通过 /sid 获取 ID。",
|
|
17
|
+
),
|
|
18
|
+
)
|
|
19
|
+
return
|
|
20
|
+
self.context.get_config()["admins_id"].append(str(admin_id))
|
|
21
|
+
self.context.get_config().save_config()
|
|
22
|
+
event.set_result(MessageEventResult().message("授权成功。"))
|
|
23
|
+
|
|
24
|
+
async def deop(self, event: AstrMessageEvent, admin_id: str = ""):
|
|
25
|
+
"""取消授权管理员。deop <admin_id>"""
|
|
26
|
+
if not admin_id:
|
|
27
|
+
event.set_result(
|
|
28
|
+
MessageEventResult().message(
|
|
29
|
+
"使用方法: /deop <id> 取消管理员。可通过 /sid 获取 ID。",
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
return
|
|
33
|
+
try:
|
|
34
|
+
self.context.get_config()["admins_id"].remove(str(admin_id))
|
|
35
|
+
self.context.get_config().save_config()
|
|
36
|
+
event.set_result(MessageEventResult().message("取消授权成功。"))
|
|
37
|
+
except ValueError:
|
|
38
|
+
event.set_result(
|
|
39
|
+
MessageEventResult().message("此用户 ID 不在管理员名单内。"),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
async def wl(self, event: AstrMessageEvent, sid: str = ""):
|
|
43
|
+
"""添加白名单。wl <sid>"""
|
|
44
|
+
if not sid:
|
|
45
|
+
event.set_result(
|
|
46
|
+
MessageEventResult().message(
|
|
47
|
+
"使用方法: /wl <id> 添加白名单;/dwl <id> 删除白名单。可通过 /sid 获取 ID。",
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
return
|
|
51
|
+
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
|
52
|
+
cfg["platform_settings"]["id_whitelist"].append(str(sid))
|
|
53
|
+
cfg.save_config()
|
|
54
|
+
event.set_result(MessageEventResult().message("添加白名单成功。"))
|
|
55
|
+
|
|
56
|
+
async def dwl(self, event: AstrMessageEvent, sid: str = ""):
|
|
57
|
+
"""删除白名单。dwl <sid>"""
|
|
58
|
+
if not sid:
|
|
59
|
+
event.set_result(
|
|
60
|
+
MessageEventResult().message(
|
|
61
|
+
"使用方法: /dwl <id> 删除白名单。可通过 /sid 获取 ID。",
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
return
|
|
65
|
+
try:
|
|
66
|
+
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
|
67
|
+
cfg["platform_settings"]["id_whitelist"].remove(str(sid))
|
|
68
|
+
cfg.save_config()
|
|
69
|
+
event.set_result(MessageEventResult().message("删除白名单成功。"))
|
|
70
|
+
except ValueError:
|
|
71
|
+
event.set_result(MessageEventResult().message("此 SID 不在白名单内。"))
|
|
72
|
+
|
|
73
|
+
async def update_dashboard(self, event: AstrMessageEvent):
|
|
74
|
+
"""更新管理面板"""
|
|
75
|
+
await event.send(MessageChain().message("正在尝试更新管理面板..."))
|
|
76
|
+
await download_dashboard(version=f"v{VERSION}", latest=False)
|
|
77
|
+
await event.send(MessageChain().message("管理面板更新完成。"))
|