AstrBot 4.10.2__py3-none-any.whl → 4.10.4__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.
Files changed (84) hide show
  1. astrbot/builtin_stars/astrbot/long_term_memory.py +186 -0
  2. astrbot/builtin_stars/astrbot/main.py +120 -0
  3. astrbot/builtin_stars/astrbot/metadata.yaml +4 -0
  4. astrbot/builtin_stars/astrbot/process_llm_request.py +245 -0
  5. astrbot/builtin_stars/builtin_commands/commands/__init__.py +31 -0
  6. astrbot/builtin_stars/builtin_commands/commands/admin.py +77 -0
  7. astrbot/builtin_stars/builtin_commands/commands/alter_cmd.py +173 -0
  8. astrbot/builtin_stars/builtin_commands/commands/conversation.py +366 -0
  9. astrbot/builtin_stars/builtin_commands/commands/help.py +88 -0
  10. astrbot/builtin_stars/builtin_commands/commands/llm.py +20 -0
  11. astrbot/builtin_stars/builtin_commands/commands/persona.py +142 -0
  12. astrbot/builtin_stars/builtin_commands/commands/plugin.py +120 -0
  13. astrbot/builtin_stars/builtin_commands/commands/provider.py +329 -0
  14. astrbot/builtin_stars/builtin_commands/commands/setunset.py +36 -0
  15. astrbot/builtin_stars/builtin_commands/commands/sid.py +36 -0
  16. astrbot/builtin_stars/builtin_commands/commands/t2i.py +23 -0
  17. astrbot/builtin_stars/builtin_commands/commands/tool.py +31 -0
  18. astrbot/builtin_stars/builtin_commands/commands/tts.py +36 -0
  19. astrbot/builtin_stars/builtin_commands/commands/utils/rst_scene.py +26 -0
  20. astrbot/builtin_stars/builtin_commands/main.py +237 -0
  21. astrbot/builtin_stars/builtin_commands/metadata.yaml +4 -0
  22. astrbot/builtin_stars/python_interpreter/main.py +536 -0
  23. astrbot/builtin_stars/python_interpreter/metadata.yaml +4 -0
  24. astrbot/builtin_stars/python_interpreter/requirements.txt +1 -0
  25. astrbot/builtin_stars/python_interpreter/shared/api.py +22 -0
  26. astrbot/builtin_stars/reminder/main.py +266 -0
  27. astrbot/builtin_stars/reminder/metadata.yaml +4 -0
  28. astrbot/builtin_stars/session_controller/main.py +114 -0
  29. astrbot/builtin_stars/session_controller/metadata.yaml +5 -0
  30. astrbot/builtin_stars/web_searcher/engines/__init__.py +111 -0
  31. astrbot/builtin_stars/web_searcher/engines/bing.py +30 -0
  32. astrbot/builtin_stars/web_searcher/engines/sogo.py +52 -0
  33. astrbot/builtin_stars/web_searcher/main.py +436 -0
  34. astrbot/builtin_stars/web_searcher/metadata.yaml +4 -0
  35. astrbot/cli/__init__.py +1 -1
  36. astrbot/core/agent/message.py +32 -1
  37. astrbot/core/agent/runners/tool_loop_agent_runner.py +26 -8
  38. astrbot/core/astr_agent_hooks.py +6 -0
  39. astrbot/core/backup/__init__.py +26 -0
  40. astrbot/core/backup/constants.py +77 -0
  41. astrbot/core/backup/exporter.py +477 -0
  42. astrbot/core/backup/importer.py +761 -0
  43. astrbot/core/config/astrbot_config.py +2 -0
  44. astrbot/core/config/default.py +47 -6
  45. astrbot/core/knowledge_base/chunking/recursive.py +10 -2
  46. astrbot/core/log.py +1 -1
  47. astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +184 -174
  48. astrbot/core/pipeline/result_decorate/stage.py +65 -57
  49. astrbot/core/pipeline/waking_check/stage.py +31 -3
  50. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +15 -29
  51. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +1 -6
  52. astrbot/core/platform/sources/dingtalk/dingtalk_event.py +15 -1
  53. astrbot/core/platform/sources/lark/lark_adapter.py +2 -10
  54. astrbot/core/platform/sources/misskey/misskey_adapter.py +0 -5
  55. astrbot/core/platform/sources/misskey/misskey_utils.py +0 -3
  56. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +4 -9
  57. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +4 -9
  58. astrbot/core/platform/sources/satori/satori_adapter.py +6 -1
  59. astrbot/core/platform/sources/slack/slack_adapter.py +3 -6
  60. astrbot/core/platform/sources/webchat/webchat_adapter.py +0 -1
  61. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +3 -5
  62. astrbot/core/provider/entities.py +41 -10
  63. astrbot/core/provider/provider.py +3 -1
  64. astrbot/core/provider/sources/anthropic_source.py +140 -30
  65. astrbot/core/provider/sources/fishaudio_tts_api_source.py +14 -6
  66. astrbot/core/provider/sources/gemini_source.py +112 -29
  67. astrbot/core/provider/sources/minimax_tts_api_source.py +4 -1
  68. astrbot/core/provider/sources/openai_source.py +93 -56
  69. astrbot/core/provider/sources/xai_source.py +29 -0
  70. astrbot/core/provider/sources/xinference_stt_provider.py +24 -12
  71. astrbot/core/star/context.py +1 -1
  72. astrbot/core/star/star_manager.py +52 -13
  73. astrbot/core/utils/astrbot_path.py +34 -0
  74. astrbot/core/utils/pip_installer.py +20 -1
  75. astrbot/dashboard/routes/__init__.py +2 -0
  76. astrbot/dashboard/routes/backup.py +1093 -0
  77. astrbot/dashboard/routes/config.py +45 -0
  78. astrbot/dashboard/routes/log.py +44 -10
  79. astrbot/dashboard/server.py +9 -1
  80. {astrbot-4.10.2.dist-info → astrbot-4.10.4.dist-info}/METADATA +1 -1
  81. {astrbot-4.10.2.dist-info → astrbot-4.10.4.dist-info}/RECORD +84 -44
  82. {astrbot-4.10.2.dist-info → astrbot-4.10.4.dist-info}/WHEEL +0 -0
  83. {astrbot-4.10.2.dist-info → astrbot-4.10.4.dist-info}/entry_points.txt +0 -0
  84. {astrbot-4.10.2.dist-info → astrbot-4.10.4.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,120 @@
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 record_llm_resp_to_ltm(self, event: AstrMessageEvent, resp: LLMResponse):
104
+ """在 LLM 响应后记录对话"""
105
+ if self.ltm and self.ltm_enabled(event):
106
+ try:
107
+ await self.ltm.after_req_llm(event, resp)
108
+ except Exception as e:
109
+ logger.error(f"ltm: {e}")
110
+
111
+ @filter.after_message_sent()
112
+ async def after_message_sent(self, event: AstrMessageEvent):
113
+ """消息发送后处理"""
114
+ if self.ltm and self.ltm_enabled(event):
115
+ try:
116
+ clean_session = event.get_extra("_clean_ltm_session", False)
117
+ if clean_session:
118
+ await self.ltm.remove_session(event)
119
+ except Exception as e:
120
+ logger.error(f"ltm: {e}")
@@ -0,0 +1,4 @@
1
+ name: astrbot
2
+ desc: AstrBot 自带插件,包含人格注入、思考内容注入、群聊上下文感知等功能的实现,禁用后将无法使用这些功能。
3
+ author: Soulter
4
+ version: 4.1.0
@@ -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("管理面板更新完成。"))