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:
Soulter
2025-08-13 09:18:49 +08:00
committed by GitHub
parent 6c1f540170
commit 369eab18ab
31 changed files with 2611 additions and 786 deletions

View File

@@ -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)

View File

@@ -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:

View File

@@ -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 生成回复

View File

@@ -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

View File

@@ -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