mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-20 02:55:08 +08:00
Refactor: 重构配置文件管理,以支持更灵活的、会话粒度的(基于 umo part)配置文件隔离 (#2328)
* refactor: 重构配置文件管理,以支持更灵活的、基于 umo part 的配置文件隔离 * Refactor: 重构配置前端页面,新增数个配置项 (#2331) * refactor: 重构配置前端页面,新增数个配置项 * feat: 完善多配置文件结构 * perf: 系统配置入口 * fix: normal config item list not display * fix: 修复 axios 请求中的上下文引用问题
This commit is contained in:
@@ -8,6 +8,7 @@ from astrbot.api.provider import ProviderRequest
|
||||
from astrbot.api.message_components import Plain, Image
|
||||
from astrbot import logger
|
||||
from collections import defaultdict
|
||||
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
||||
|
||||
"""
|
||||
聊天记忆增强
|
||||
@@ -15,28 +16,40 @@ from collections import defaultdict
|
||||
|
||||
|
||||
class LongTermMemory:
|
||||
def __init__(self, config: dict, context: star.Context):
|
||||
self.config = config
|
||||
def __init__(self, acm: AstrBotConfigManager, context: star.Context):
|
||||
self.acm = acm
|
||||
self.context = context
|
||||
self.session_chats = defaultdict(list)
|
||||
"""记录群成员的群聊记录"""
|
||||
|
||||
def cfg(self, event: AstrMessageEvent):
|
||||
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
||||
try:
|
||||
self.max_cnt = int(self.config["group_message_max_cnt"])
|
||||
max_cnt = int(cfg["group_message_max_cnt"])
|
||||
except BaseException as e:
|
||||
logger.error(e)
|
||||
self.max_cnt = 300
|
||||
self.image_caption = self.config["image_caption"]
|
||||
self.image_caption_prompt = self.config["image_caption_prompt"]
|
||||
self.image_caption_provider_id = self.config["image_caption_provider_id"]
|
||||
|
||||
self.active_reply = self.config["active_reply"]
|
||||
self.enable_active_reply = self.active_reply.get("enable", False)
|
||||
self.ar_method = self.active_reply["method"]
|
||||
self.ar_possibility = self.active_reply["possibility_reply"]
|
||||
self.ar_prompt = self.active_reply.get("prompt", "")
|
||||
self.ar_whitelist = self.active_reply.get("whitelist", [])
|
||||
|
||||
# self.put_history_to_prompt = self.config["put_history_to_prompt"]
|
||||
max_cnt = 300
|
||||
image_caption = cfg["image_caption"]
|
||||
image_caption_prompt = cfg["image_caption_prompt"] # TODO: 去掉这个配置项
|
||||
image_caption_provider_id = cfg["image_caption_provider_id"] # TODO: 去掉这个配置项
|
||||
active_reply = cfg["active_reply"]
|
||||
enable_active_reply = active_reply.get("enable", False)
|
||||
ar_method = active_reply["method"]
|
||||
ar_possibility = active_reply["possibility_reply"]
|
||||
ar_prompt = active_reply.get("prompt", "")
|
||||
ar_whitelist = active_reply.get("whitelist", [])
|
||||
ret = {
|
||||
"max_cnt": max_cnt,
|
||||
"image_caption": image_caption,
|
||||
"image_caption_prompt": image_caption_prompt,
|
||||
"image_caption_provider_id": image_caption_provider_id,
|
||||
"enable_active_reply": enable_active_reply,
|
||||
"ar_method": ar_method,
|
||||
"ar_possibility": ar_possibility,
|
||||
"ar_prompt": ar_prompt,
|
||||
"ar_whitelist": ar_whitelist,
|
||||
}
|
||||
return ret
|
||||
|
||||
async def remove_session(self, event: AstrMessageEvent) -> int:
|
||||
cnt = 0
|
||||
@@ -45,17 +58,17 @@ class LongTermMemory:
|
||||
del self.session_chats[event.unified_msg_origin]
|
||||
return cnt
|
||||
|
||||
async def get_image_caption(self, image_url: str) -> str:
|
||||
if not self.image_caption_provider_id:
|
||||
async def get_image_caption(
|
||||
self, image_url: str, image_caption_provider_id: str, image_caption_prompt: str
|
||||
) -> str:
|
||||
if not image_caption_provider_id:
|
||||
provider = self.context.get_using_provider()
|
||||
else:
|
||||
provider = self.context.get_provider_by_id(self.image_caption_provider_id)
|
||||
provider = self.context.get_provider_by_id(image_caption_provider_id)
|
||||
if not provider:
|
||||
raise Exception(
|
||||
f"没有找到 ID 为 {self.image_caption_provider_id} 的提供商"
|
||||
)
|
||||
raise Exception(f"没有找到 ID 为 {image_caption_provider_id} 的提供商")
|
||||
response = await provider.text_chat(
|
||||
prompt=self.image_caption_prompt,
|
||||
prompt=image_caption_prompt,
|
||||
session_id=uuid.uuid4().hex,
|
||||
image_urls=[image_url],
|
||||
persist=False,
|
||||
@@ -63,7 +76,8 @@ class LongTermMemory:
|
||||
return response.completion_text
|
||||
|
||||
async def need_active_reply(self, event: AstrMessageEvent) -> bool:
|
||||
if not self.enable_active_reply:
|
||||
cfg = self.cfg(event)
|
||||
if not cfg["enable_active_reply"]:
|
||||
return False
|
||||
if event.get_message_type() != MessageType.GROUP_MESSAGE:
|
||||
return False
|
||||
@@ -72,15 +86,15 @@ class LongTermMemory:
|
||||
# if the message is a command, let it pass
|
||||
return False
|
||||
|
||||
if self.ar_whitelist and (
|
||||
event.unified_msg_origin not in self.ar_whitelist
|
||||
and (event.get_group_id() and event.get_group_id() not in self.ar_whitelist)
|
||||
if cfg["ar_whitelist"] and (
|
||||
event.unified_msg_origin not in cfg["ar_whitelist"]
|
||||
and (event.get_group_id() and event.get_group_id() not in cfg["ar_whitelist"])
|
||||
):
|
||||
return False
|
||||
|
||||
match self.ar_method:
|
||||
match cfg["ar_method"]:
|
||||
case "possibility_reply":
|
||||
trig = random.random() < self.ar_possibility
|
||||
trig = random.random() < cfg["ar_possibility"]
|
||||
return trig
|
||||
|
||||
return False
|
||||
@@ -92,15 +106,19 @@ class LongTermMemory:
|
||||
|
||||
final_message = f"[{event.message_obj.sender.nickname}/{datetime_str}]: "
|
||||
|
||||
cfg = self.cfg(event)
|
||||
|
||||
for comp in event.get_messages():
|
||||
if isinstance(comp, Plain):
|
||||
final_message += f" {comp.text}"
|
||||
elif isinstance(comp, Image):
|
||||
# image_urls.append(comp.url if comp.url else comp.file)
|
||||
if self.image_caption:
|
||||
cfg = self.cfg(event)
|
||||
if cfg["image_caption"]:
|
||||
try:
|
||||
caption = await self.get_image_caption(
|
||||
comp.url if comp.url else comp.file
|
||||
comp.url if comp.url else comp.file,
|
||||
cfg["image_caption_provider_id"],
|
||||
cfg["image_caption_prompt"],
|
||||
)
|
||||
final_message += f" [Image: {caption}]"
|
||||
except Exception as e:
|
||||
@@ -109,7 +127,7 @@ class LongTermMemory:
|
||||
final_message += " [Image]"
|
||||
logger.debug(f"ltm | {event.unified_msg_origin} | {final_message}")
|
||||
self.session_chats[event.unified_msg_origin].append(final_message)
|
||||
if len(self.session_chats[event.unified_msg_origin]) > self.max_cnt:
|
||||
if len(self.session_chats[event.unified_msg_origin]) > cfg["max_cnt"]:
|
||||
self.session_chats[event.unified_msg_origin].pop(0)
|
||||
|
||||
async def on_req_llm(self, event: AstrMessageEvent, req: ProviderRequest):
|
||||
@@ -119,7 +137,8 @@ class LongTermMemory:
|
||||
|
||||
chats_str = "\n---\n".join(self.session_chats[event.unified_msg_origin])
|
||||
|
||||
if self.enable_active_reply:
|
||||
cfg = self.cfg(event)
|
||||
if cfg["enable_active_reply"]:
|
||||
prompt = req.prompt
|
||||
req.prompt = f"You are now in a chatroom. The chat history is as follows:\n{chats_str}"
|
||||
req.prompt += f"\nNow, a new message is coming: `{prompt}`. Please react to it. Only output your response and do not output any other information."
|
||||
@@ -138,5 +157,6 @@ class LongTermMemory:
|
||||
final_message = f"[You/{datetime.datetime.now().strftime('%H:%M:%S')}]: {event.get_result().get_plain_text()}"
|
||||
logger.debug(f"ltm | {event.unified_msg_origin} | {final_message}")
|
||||
self.session_chats[event.unified_msg_origin].append(final_message)
|
||||
if len(self.session_chats[event.unified_msg_origin]) > self.max_cnt:
|
||||
cfg = self.cfg(event)
|
||||
if len(self.session_chats[event.unified_msg_origin]) > cfg["max_cnt"]:
|
||||
self.session_chats[event.unified_msg_origin].pop(0)
|
||||
|
||||
@@ -60,9 +60,6 @@ class Main(star.Star):
|
||||
def __init__(self, context: star.Context) -> None:
|
||||
self.context = context
|
||||
cfg = context.get_config()
|
||||
self.prompt_prefix = cfg["provider_settings"]["prompt_prefix"]
|
||||
self.identifier = cfg["provider_settings"]["identifier"]
|
||||
self.enable_datetime = cfg["provider_settings"]["datetime_system_prompt"]
|
||||
self.timezone = cfg.get("timezone")
|
||||
if not self.timezone:
|
||||
# 系统默认时区
|
||||
@@ -70,18 +67,10 @@ class Main(star.Star):
|
||||
else:
|
||||
logger.info(f"Timezone set to: {self.timezone}")
|
||||
self.ltm = None
|
||||
if (
|
||||
self.context.get_config()["provider_ltm_settings"]["group_icl_enable"]
|
||||
or self.context.get_config()["provider_ltm_settings"]["active_reply"][
|
||||
"enable"
|
||||
]
|
||||
):
|
||||
try:
|
||||
self.ltm = LongTermMemory(
|
||||
self.context.get_config()["provider_ltm_settings"], self.context
|
||||
)
|
||||
except BaseException as e:
|
||||
logger.error(f"聊天增强 err: {e}")
|
||||
try:
|
||||
self.ltm = LongTermMemory(self.context.astrbot_config_mgr, self.context)
|
||||
except BaseException as e:
|
||||
logger.error(f"聊天增强 err: {e}")
|
||||
|
||||
async def _query_astrbot_notice(self):
|
||||
try:
|
||||
@@ -93,6 +82,12 @@ class Main(star.Star):
|
||||
except BaseException:
|
||||
return ""
|
||||
|
||||
def ltm_enabled(self, event: AstrMessageEvent):
|
||||
ltmse = self.context.get_config(umo=event.unified_msg_origin)[
|
||||
"provider_ltm_settings"
|
||||
]
|
||||
return ltmse["group_icl_enable"] or ltmse["active_reply"]["enable"]
|
||||
|
||||
@filter.command("help")
|
||||
async def help(self, event: AstrMessageEvent):
|
||||
"""查看帮助"""
|
||||
@@ -139,7 +134,7 @@ class Main(star.Star):
|
||||
@filter.command("llm")
|
||||
async def llm(self, event: AstrMessageEvent):
|
||||
"""开启/关闭 LLM"""
|
||||
cfg = self.context.get_config()
|
||||
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
||||
enable = cfg["provider_settings"]["enable"]
|
||||
if enable:
|
||||
cfg["provider_settings"]["enable"] = False
|
||||
@@ -294,7 +289,7 @@ class Main(star.Star):
|
||||
@filter.command("t2i")
|
||||
async def t2i(self, event: AstrMessageEvent):
|
||||
"""开关文本转图片"""
|
||||
config = self.context.get_config()
|
||||
config = self.context.get_config(umo=event.unified_msg_origin)
|
||||
if config["t2i"]:
|
||||
config["t2i"] = False
|
||||
config.save_config()
|
||||
@@ -376,8 +371,9 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
"使用方法: /wl <id> 添加白名单;/dwl <id> 删除白名单。可通过 /sid 获取 ID。"
|
||||
)
|
||||
)
|
||||
self.context.get_config()["platform_settings"]["id_whitelist"].append(str(sid))
|
||||
self.context.get_config().save_config()
|
||||
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
||||
cfg["platform_settings"]["id_whitelist"].append(str(sid))
|
||||
cfg.save_config()
|
||||
event.set_result(MessageEventResult().message("添加白名单成功。"))
|
||||
|
||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||
@@ -385,10 +381,9 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
async def dwl(self, event: AstrMessageEvent, sid: str):
|
||||
"""删除白名单。dwl <sid>"""
|
||||
try:
|
||||
self.context.get_config()["platform_settings"]["id_whitelist"].remove(
|
||||
str(sid)
|
||||
)
|
||||
self.context.get_config().save_config()
|
||||
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
||||
cfg["platform_settings"]["id_whitelist"].remove(str(sid))
|
||||
cfg.save_config()
|
||||
event.set_result(MessageEventResult().message("删除白名单成功。"))
|
||||
except ValueError:
|
||||
event.set_result(MessageEventResult().message("此 SID 不在白名单内。"))
|
||||
@@ -551,7 +546,7 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
)
|
||||
|
||||
ret = "清除会话 LLM 聊天历史成功。"
|
||||
if self.ltm:
|
||||
if self.ltm and self.ltm_enabled(message):
|
||||
cnt = await self.ltm.remove_session(event=message)
|
||||
ret += f"\n聊天增强: 已清除 {cnt} 条聊天记录。"
|
||||
|
||||
@@ -769,7 +764,7 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
)
|
||||
|
||||
# 长期记忆
|
||||
if self.ltm:
|
||||
if self.ltm and self.ltm_enabled(message):
|
||||
try:
|
||||
await self.ltm.remove_session(event=message)
|
||||
except Exception as e:
|
||||
@@ -1137,7 +1132,7 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
has_image_or_plain = True
|
||||
break
|
||||
|
||||
if self.ltm and has_image_or_plain:
|
||||
if self.ltm_enabled(event) and self.ltm and has_image_or_plain:
|
||||
need_active = await self.ltm.need_active_reply(event)
|
||||
|
||||
group_icl_enable = self.context.get_config()["provider_ltm_settings"][
|
||||
@@ -1205,8 +1200,9 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
@filter.on_llm_request()
|
||||
async def decorate_llm_req(self, event: AstrMessageEvent, req: ProviderRequest):
|
||||
"""在请求 LLM 前注入人格信息、Identifier、时间、回复内容等 System Prompt"""
|
||||
if self.prompt_prefix:
|
||||
req.prompt = self.prompt_prefix + req.prompt
|
||||
cfg = self.context.get_config(umo=event.unified_msg_origin)["provider_settings"]
|
||||
if prefix := cfg.get("prompt_prefix"):
|
||||
req.prompt = prefix + req.prompt
|
||||
|
||||
# 解析引用内容
|
||||
quote = None
|
||||
@@ -1215,14 +1211,14 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
quote = comp
|
||||
break
|
||||
|
||||
if self.identifier:
|
||||
if cfg.get("identifier"):
|
||||
user_id = event.message_obj.sender.user_id
|
||||
user_nickname = event.message_obj.sender.nickname
|
||||
user_info = f"\n[User ID: {user_id}, Nickname: {user_nickname}]\n"
|
||||
req.prompt = user_info + req.prompt
|
||||
|
||||
# 启用附加时间戳
|
||||
if self.enable_datetime:
|
||||
if cfg.get("datetime_system_prompt"):
|
||||
current_time = None
|
||||
if self.timezone:
|
||||
# 启用时区
|
||||
@@ -1300,7 +1296,7 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
except BaseException as e:
|
||||
logger.error(f"处理引用图片失败: {e}")
|
||||
|
||||
if self.ltm:
|
||||
if self.ltm and self.ltm_enabled(event):
|
||||
try:
|
||||
await self.ltm.on_req_llm(event, req)
|
||||
except BaseException as e:
|
||||
@@ -1309,7 +1305,7 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
@filter.after_message_sent()
|
||||
async def after_llm_req(self, event: AstrMessageEvent):
|
||||
"""在 LLM 请求后记录对话"""
|
||||
if self.ltm:
|
||||
if self.ltm and self.ltm_enabled(event):
|
||||
try:
|
||||
await self.ltm.after_req_llm(event)
|
||||
except BaseException as e:
|
||||
|
||||
@@ -19,9 +19,6 @@ class Waiter(Star):
|
||||
def __init__(self, context: Context):
|
||||
super().__init__(context)
|
||||
|
||||
self.p_settings: dict = self.context.get_config()["platform_settings"]
|
||||
self.wake_prefix = self.context.get_config()["wake_prefix"]
|
||||
|
||||
@filter.event_message_type(filter.EventMessageType.ALL, priority=maxsize)
|
||||
async def handle_session_control_agent(self, event: AstrMessageEvent):
|
||||
"""会话控制代理"""
|
||||
@@ -36,16 +33,19 @@ class Waiter(Star):
|
||||
"""实现了对只有一个 @ 的消息内容的处理"""
|
||||
try:
|
||||
messages = event.get_messages()
|
||||
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
||||
p_settings = cfg["platform_settings"]
|
||||
wake_prefix = cfg.get("wake_prefix", [])
|
||||
if len(messages) == 1:
|
||||
if (
|
||||
isinstance(messages[0], Comp.At)
|
||||
and str(messages[0].qq) == str(event.get_self_id())
|
||||
and self.p_settings.get("empty_mention_waiting", True)
|
||||
and p_settings.get("empty_mention_waiting", True)
|
||||
) or (
|
||||
isinstance(messages[0], Comp.Plain)
|
||||
and messages[0].text.strip() in self.wake_prefix
|
||||
and messages[0].text.strip() in wake_prefix
|
||||
):
|
||||
if self.p_settings.get("empty_mention_waiting_need_reply", True):
|
||||
if p_settings.get("empty_mention_waiting_need_reply", True):
|
||||
try:
|
||||
# 尝试使用 LLM 生成更生动的回复
|
||||
func_tools_mgr = self.context.get_llm_tool_manager()
|
||||
@@ -63,7 +63,8 @@ class Waiter(Star):
|
||||
else:
|
||||
# 创建新对话
|
||||
curr_cid = await self.context.conversation_manager.new_conversation(
|
||||
event.unified_msg_origin, platform_id=event.get_platform_id()
|
||||
event.unified_msg_origin,
|
||||
platform_id=event.get_platform_id(),
|
||||
)
|
||||
|
||||
# 使用 LLM 生成回复
|
||||
|
||||
@@ -8,15 +8,13 @@ from openai.types.chat.chat_completion import ChatCompletion
|
||||
class R1Filter(Star):
|
||||
def __init__(self, context: Context):
|
||||
super().__init__(context)
|
||||
self.display_reasoning_text = (
|
||||
self.context.get_config()
|
||||
.get("provider_settings", {})
|
||||
.get("display_reasoning_text", False)
|
||||
)
|
||||
|
||||
@filter.on_llm_response()
|
||||
async def resp(self, event: AstrMessageEvent, response: LLMResponse):
|
||||
if self.display_reasoning_text:
|
||||
cfg = self.context.get_config(umo=event.unified_msg_origin).get(
|
||||
"provider_settings", {}
|
||||
)
|
||||
if cfg.get("display_reasoning_text", False):
|
||||
# 显示推理内容的处理逻辑
|
||||
if (
|
||||
response
|
||||
|
||||
@@ -22,19 +22,6 @@ class Main(star.Star):
|
||||
self.sogo_search = Sogo()
|
||||
self.google = Google()
|
||||
|
||||
self.websearch_link = self.context.get_config()["provider_settings"].get(
|
||||
"web_search_link", False
|
||||
)
|
||||
|
||||
async def initialize(self):
|
||||
websearch = self.context.get_config()["provider_settings"]["web_search"]
|
||||
if websearch:
|
||||
self.context.activate_llm_tool("web_search")
|
||||
self.context.activate_llm_tool("fetch_url")
|
||||
else:
|
||||
self.context.deactivate_llm_tool("web_search")
|
||||
self.context.deactivate_llm_tool("fetch_url")
|
||||
|
||||
async def _tidy_text(self, text: str) -> str:
|
||||
"""清理文本,去除空格、换行符等"""
|
||||
return text.strip().replace("\n", " ").replace("\r", " ").replace(" ", " ")
|
||||
@@ -54,34 +41,7 @@ class Main(star.Star):
|
||||
|
||||
@filter.command("websearch")
|
||||
async def websearch(self, event: AstrMessageEvent, oper: str = None) -> str:
|
||||
websearch = self.context.get_config()["provider_settings"]["web_search"]
|
||||
if oper is None:
|
||||
status = "开启" if websearch else "关闭"
|
||||
event.set_result(
|
||||
MessageEventResult().message(
|
||||
"当前网页搜索功能状态:"
|
||||
+ status
|
||||
+ "。使用 /websearch on 或者 off 启用或者关闭。"
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if oper == "on":
|
||||
self.context.get_config()["provider_settings"]["web_search"] = True
|
||||
self.context.get_config().save_config()
|
||||
self.context.activate_llm_tool("web_search")
|
||||
self.context.activate_llm_tool("fetch_url")
|
||||
event.set_result(MessageEventResult().message("已开启网页搜索功能"))
|
||||
elif oper == "off":
|
||||
self.context.get_config()["provider_settings"]["web_search"] = False
|
||||
self.context.get_config().save_config()
|
||||
self.context.deactivate_llm_tool("web_search")
|
||||
self.context.deactivate_llm_tool("fetch_url")
|
||||
event.set_result(MessageEventResult().message("已关闭网页搜索功能"))
|
||||
else:
|
||||
event.set_result(
|
||||
MessageEventResult().message("操作参数错误,应为 on 或 off")
|
||||
)
|
||||
event.set_result(MessageEventResult().message("此指令已经被废弃,请在 WebUI 中开启或关闭网页搜索功能。"))
|
||||
|
||||
@llm_tool("web_search")
|
||||
async def search_from_search_engine(
|
||||
@@ -93,6 +53,9 @@ class Main(star.Star):
|
||||
query(string): 和用户的问题最相关的搜索关键词,用于在 Google 上搜索。
|
||||
"""
|
||||
logger.info("web_searcher - search_from_search_engine: " + query)
|
||||
websearch_link = self.context.get_config(umo=event.unified_msg_origin)[
|
||||
"provider_settings"
|
||||
].get("web_search_link", False)
|
||||
results = []
|
||||
RESULT_NUM = 5
|
||||
try:
|
||||
@@ -128,13 +91,13 @@ class Main(star.Star):
|
||||
|
||||
header = f"{idx}. {i.title} "
|
||||
|
||||
if self.websearch_link and i.url:
|
||||
if websearch_link and i.url:
|
||||
header += i.url
|
||||
|
||||
ret += f"{header}\n{i.snippet}\n{site_result}\n\n"
|
||||
idx += 1
|
||||
|
||||
if self.websearch_link:
|
||||
if websearch_link:
|
||||
ret += "针对问题,请根据上面的结果分点总结,并且在结尾处附上对应内容的参考链接(如有)。"
|
||||
|
||||
return ret
|
||||
|
||||
Reference in New Issue
Block a user