AstrBot 4.0.0b5__py3-none-any.whl → 4.1.1__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 (33) hide show
  1. astrbot/api/event/filter/__init__.py +2 -0
  2. astrbot/core/config/default.py +73 -3
  3. astrbot/core/initial_loader.py +4 -1
  4. astrbot/core/message/components.py +59 -50
  5. astrbot/core/pipeline/result_decorate/stage.py +5 -1
  6. astrbot/core/platform/manager.py +25 -3
  7. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +11 -4
  8. astrbot/core/platform/sources/satori/satori_adapter.py +482 -0
  9. astrbot/core/platform/sources/satori/satori_event.py +221 -0
  10. astrbot/core/platform/sources/telegram/tg_adapter.py +0 -1
  11. astrbot/core/provider/sources/openai_source.py +14 -5
  12. astrbot/core/provider/sources/vllm_rerank_source.py +6 -0
  13. astrbot/core/star/__init__.py +7 -5
  14. astrbot/core/star/filter/command.py +9 -3
  15. astrbot/core/star/filter/platform_adapter_type.py +3 -0
  16. astrbot/core/star/register/__init__.py +2 -0
  17. astrbot/core/star/register/star_handler.py +18 -4
  18. astrbot/core/star/star_handler.py +9 -1
  19. astrbot/core/star/star_tools.py +116 -21
  20. astrbot/core/utils/t2i/network_strategy.py +11 -18
  21. astrbot/core/utils/t2i/renderer.py +8 -2
  22. astrbot/core/utils/t2i/template/astrbot_powershell.html +184 -0
  23. astrbot/core/utils/t2i/template_manager.py +112 -0
  24. astrbot/dashboard/routes/chat.py +6 -1
  25. astrbot/dashboard/routes/config.py +10 -49
  26. astrbot/dashboard/routes/route.py +19 -2
  27. astrbot/dashboard/routes/t2i.py +230 -0
  28. astrbot/dashboard/server.py +13 -4
  29. {astrbot-4.0.0b5.dist-info → astrbot-4.1.1.dist-info}/METADATA +39 -52
  30. {astrbot-4.0.0b5.dist-info → astrbot-4.1.1.dist-info}/RECORD +33 -28
  31. {astrbot-4.0.0b5.dist-info → astrbot-4.1.1.dist-info}/WHEEL +0 -0
  32. {astrbot-4.0.0b5.dist-info → astrbot-4.1.1.dist-info}/entry_points.txt +0 -0
  33. {astrbot-4.0.0b5.dist-info → astrbot-4.1.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,15 +1,37 @@
1
+ """
2
+ 插件开发工具集
3
+ 封装了许多常用的操作,方便插件开发者使用
4
+
5
+ 说明:
6
+
7
+ 主动发送消息: send_message(session, message_chain)
8
+ 根据 session (unified_msg_origin) 主动发送消息, 前提是需要提前获得或构造 session
9
+
10
+ 根据id直接主动发送消息: send_message_by_id(type, id, message_chain, platform="aiocqhttp")
11
+ 根据 id (例如 qq 号, 群号等) 直接, 主动地发送消息
12
+
13
+ 以上两种方式需要构造消息链, 也就是消息组件的列表
14
+
15
+ 构造事件:
16
+
17
+ 首先需要构造一个 AstrBotMessage 对象, 使用 create_message 方法
18
+ 然后使用 create_event 方法提交事件到指定平台
19
+ """
20
+
1
21
  import inspect
2
22
  import os
23
+ import uuid
3
24
  from pathlib import Path
4
25
  from typing import Union, Awaitable, List, Optional, ClassVar
5
26
  from astrbot.core.message.components import BaseMessageComponent
6
27
  from astrbot.core.message.message_event_result import MessageChain
7
- from astrbot.api.platform import MessageMember, AstrBotMessage
28
+ from astrbot.api.platform import MessageMember, AstrBotMessage, MessageType
8
29
  from astrbot.core.platform.astr_message_event import MessageSesion
9
30
  from astrbot.core.star.context import Context
10
31
  from astrbot.core.star.star import star_map
11
32
  from astrbot.core.utils.astrbot_path import get_astrbot_data_path
12
-
33
+ from astrbot.core.platform.sources.aiocqhttp.aiocqhttp_message_event import AiocqhttpMessageEvent
34
+ from astrbot.core.platform.sources.aiocqhttp.aiocqhttp_platform_adapter import AiocqhttpAdapter
13
35
 
14
36
  class StarTools:
15
37
  """
@@ -49,42 +71,76 @@ class StarTools:
49
71
  Note:
50
72
  qq_official(QQ官方API平台)不支持此方法
51
73
  """
74
+ if cls._context is None:
75
+ raise ValueError("StarTools not initialized")
52
76
  return await cls._context.send_message(session, message_chain)
53
77
 
78
+ @classmethod
79
+ async def send_message_by_id(
80
+ cls, type: str, id: str, message_chain: MessageChain, platform: str = "aiocqhttp"
81
+ ):
82
+ """
83
+ 根据 id(例如qq号, 群号等) 直接, 主动地发送消息
84
+
85
+ Args:
86
+ type (str): 消息类型, 可选: PrivateMessage, GroupMessage
87
+ id (str): 目标ID, 例如QQ号, 群号等
88
+ message_chain (MessageChain): 消息链
89
+ platform (str): 可选的平台名称,默认平台(aiocqhttp), 目前只支持 aiocqhttp
90
+ """
91
+ if cls._context is None:
92
+ raise ValueError("StarTools not initialized")
93
+ platforms = cls._context.platform_manager.get_insts()
94
+ if platform == "aiocqhttp":
95
+ adapter = next((p for p in platforms if isinstance(p, AiocqhttpAdapter)), None)
96
+ if adapter is None:
97
+ raise ValueError("未找到适配器: AiocqhttpAdapter")
98
+ await AiocqhttpMessageEvent.send_message(
99
+ bot=adapter.bot,
100
+ message_chain=message_chain,
101
+ is_group=(type == "GroupMessage"),
102
+ session_id=id,
103
+ )
104
+ else:
105
+ raise ValueError(f"不支持的平台: {platform}")
106
+
54
107
  @classmethod
55
108
  async def create_message(
56
109
  cls,
57
110
  type: str,
58
111
  self_id: str,
59
112
  session_id: str,
60
- message_id: str,
61
113
  sender: MessageMember,
62
114
  message: List[BaseMessageComponent],
63
115
  message_str: str,
64
- raw_message: object,
65
- group_id: str = "",
66
- ):
116
+ message_id: str = "",
117
+ raw_message: object = None,
118
+ group_id: str = ""
119
+ ) -> AstrBotMessage:
67
120
  """
68
121
  创建一个AstrBot消息对象
69
122
 
70
123
  Args:
71
- type (str): 消息类型
124
+ type (str): 消息类型, 例如 "GroupMessage" "FriendMessage" "OtherMessage"
72
125
  self_id (str): 机器人自身ID
73
126
  session_id (str): 会话ID(通常为用户ID)(QQ号, 群号等)
74
- message_id (str): 消息ID
75
- sender (MessageMember): 发送者信息
76
- message (List[BaseMessageComponent]): 消息组件列表
77
- message_str (str): 消息字符串
78
- raw_message (object): 原始消息对象
127
+ sender (MessageMember): 发送者信息, 例如 MessageMember(user_id="123456", nickname="昵称")
128
+ message (List[BaseMessageComponent]): 消息组件列表, 也就是消息链, 这个不会发给 llm, 但是会经过其他处理
129
+ message_str (str): 消息字符串, 也就是纯文本消息, 也就是发送给 llm 的消息, 与消息链一致
130
+
131
+ message_id (str): 消息ID, 构造消息时可以随意填写也可不填
132
+ raw_message (object): 原始消息对象, 可以随意填写也可不填
79
133
  group_id (str, optional): 群组ID, 如果为私聊则为空. Defaults to "".
80
134
 
81
135
  Returns:
82
136
  AstrBotMessage: 创建的消息对象
83
137
  """
84
138
  abm = AstrBotMessage()
85
- abm.type = type
139
+ abm.type = MessageType(type)
86
140
  abm.self_id = self_id
87
141
  abm.session_id = session_id
142
+ if message_id == "":
143
+ message_id = uuid.uuid4().hex
88
144
  abm.message_id = message_id
89
145
  abm.sender = sender
90
146
  abm.message = message
@@ -93,13 +149,38 @@ class StarTools:
93
149
  abm.group_id = group_id
94
150
  return abm
95
151
 
96
- # todo: 添加构造事件的方法
97
- # async def create_event(
98
- # self, platform: str, umo: str, sender_id: str, session_id: str
99
- # ):
100
- # platform = self._context.get_platform(platform)
152
+ @classmethod
153
+ async def create_event(
154
+ cls, abm: AstrBotMessage, platform: str = "aiocqhttp", is_wake: bool = True
155
+
156
+ ) -> None:
157
+ """
158
+ 创建并提交事件到指定平台
159
+ 当有需要创建一个事件, 触发某些处理流程时, 使用该方法
101
160
 
102
- # todo: 添加找到对应平台并提交对应事件的方法
161
+ Args:
162
+ abm (AstrBotMessage): 要提交的消息对象, 请先使用 create_message 创建
163
+ platform (str): 可选的平台名称,默认平台(aiocqhttp), 目前只支持 aiocqhttp
164
+ is_wake (bool): 是否标记为唤醒事件, 默认为 True, 只有唤醒事件才会被 llm 响应
165
+ """
166
+ if cls._context is None:
167
+ raise ValueError("StarTools not initialized")
168
+ platforms = cls._context.platform_manager.get_insts()
169
+ if platform == "aiocqhttp":
170
+ adapter = next((p for p in platforms if isinstance(p, AiocqhttpAdapter)), None)
171
+ if adapter is None:
172
+ raise ValueError("未找到适配器: AiocqhttpAdapter")
173
+ event = AiocqhttpMessageEvent(
174
+ message_str=abm.message_str,
175
+ message_obj=abm,
176
+ platform_meta=adapter.metadata,
177
+ session_id=abm.session_id,
178
+ bot=adapter.bot,
179
+ )
180
+ event.is_wake = is_wake
181
+ adapter.commit_event(event)
182
+ else:
183
+ raise ValueError(f"不支持的平台: {platform}")
103
184
 
104
185
  @classmethod
105
186
  def activate_llm_tool(cls, name: str) -> bool:
@@ -110,6 +191,8 @@ class StarTools:
110
191
  Args:
111
192
  name (str): 工具名称
112
193
  """
194
+ if cls._context is None:
195
+ raise ValueError("StarTools not initialized")
113
196
  return cls._context.activate_llm_tool(name)
114
197
 
115
198
  @classmethod
@@ -120,6 +203,8 @@ class StarTools:
120
203
  Args:
121
204
  name (str): 工具名称
122
205
  """
206
+ if cls._context is None:
207
+ raise ValueError("StarTools not initialized")
123
208
  return cls._context.deactivate_llm_tool(name)
124
209
 
125
210
  @classmethod
@@ -135,6 +220,8 @@ class StarTools:
135
220
  desc (str): 工具描述
136
221
  func_obj (Awaitable): 函数对象,必须是异步函数
137
222
  """
223
+ if cls._context is None:
224
+ raise ValueError("StarTools not initialized")
138
225
  cls._context.register_llm_tool(name, func_args, desc, func_obj)
139
226
 
140
227
  @classmethod
@@ -146,6 +233,8 @@ class StarTools:
146
233
  Args:
147
234
  name (str): 工具名称
148
235
  """
236
+ if cls._context is None:
237
+ raise ValueError("StarTools not initialized")
149
238
  cls._context.unregister_llm_tool(name)
150
239
 
151
240
  @classmethod
@@ -169,8 +258,11 @@ class StarTools:
169
258
  - 创建目录失败(权限不足或其他IO错误)
170
259
  """
171
260
  if not plugin_name:
172
- frame = inspect.currentframe().f_back
173
- module = inspect.getmodule(frame)
261
+ frame = inspect.currentframe()
262
+ module = None
263
+ if frame:
264
+ frame = frame.f_back
265
+ module = inspect.getmodule(frame)
174
266
 
175
267
  if not module:
176
268
  raise RuntimeError("无法获取调用者模块信息")
@@ -182,6 +274,9 @@ class StarTools:
182
274
 
183
275
  plugin_name = metadata.name
184
276
 
277
+ if not plugin_name:
278
+ raise ValueError("无法获取插件名称")
279
+
185
280
  data_dir = Path(os.path.join(get_astrbot_data_path(), "plugin_data", plugin_name))
186
281
 
187
282
  try:
@@ -1,6 +1,5 @@
1
1
  import aiohttp
2
2
  import asyncio
3
- import os
4
3
  import ssl
5
4
  import certifi
6
5
  import logging
@@ -8,10 +7,9 @@ import random
8
7
  from . import RenderStrategy
9
8
  from astrbot.core.config import VERSION
10
9
  from astrbot.core.utils.io import download_image_by_url
11
- from astrbot.core.utils.astrbot_path import get_astrbot_data_path
10
+ from astrbot.core.utils.t2i.template_manager import TemplateManager
12
11
 
13
12
  ASTRBOT_T2I_DEFAULT_ENDPOINT = "https://t2i.soulter.top/text2img"
14
- CUSTOM_T2I_TEMPLATE_PATH = os.path.join(get_astrbot_data_path(), "t2i_template.html")
15
13
 
16
14
  logger = logging.getLogger("astrbot")
17
15
 
@@ -23,26 +21,17 @@ class NetworkRenderStrategy(RenderStrategy):
23
21
  self.BASE_RENDER_URL = ASTRBOT_T2I_DEFAULT_ENDPOINT
24
22
  else:
25
23
  self.BASE_RENDER_URL = self._clean_url(base_url)
26
- self.TEMPLATE_PATH = os.path.join(os.path.dirname(__file__), "template", "base.html")
27
- with open(self.TEMPLATE_PATH, "r", encoding="utf-8") as f:
28
- self.DEFAULT_TEMPLATE = f.read()
29
24
 
30
25
  self.endpoints = [self.BASE_RENDER_URL]
26
+ self.template_manager = TemplateManager()
31
27
 
32
28
  async def initialize(self):
33
29
  if self.BASE_RENDER_URL == ASTRBOT_T2I_DEFAULT_ENDPOINT:
34
30
  asyncio.create_task(self.get_official_endpoints())
35
31
 
36
- async def get_template(self) -> str:
37
- """获取文转图 HTML 模板
38
-
39
- Returns:
40
- str: 文转图 HTML 模板字符串
41
- """
42
- if os.path.exists(CUSTOM_T2I_TEMPLATE_PATH):
43
- with open(CUSTOM_T2I_TEMPLATE_PATH, "r", encoding="utf-8") as f:
44
- return f.read()
45
- return self.DEFAULT_TEMPLATE
32
+ async def get_template(self, name: str = "base") -> str:
33
+ """通过名称获取文转图 HTML 模板"""
34
+ return self.template_manager.get_template(name)
46
35
 
47
36
  async def get_official_endpoints(self):
48
37
  """获取官方的 t2i 端点列表。"""
@@ -124,11 +113,15 @@ class NetworkRenderStrategy(RenderStrategy):
124
113
  logger.error(f"All endpoints failed: {last_exception}")
125
114
  raise RuntimeError(f"All endpoints failed: {last_exception}")
126
115
 
127
- async def render(self, text: str, return_url: bool = False) -> str:
116
+ async def render(
117
+ self, text: str, return_url: bool = False, template_name: str | None = "base"
118
+ ) -> str:
128
119
  """
129
120
  返回图像的文件路径
130
121
  """
131
- tmpl_str = await self.get_template()
122
+ if not template_name:
123
+ template_name = "base"
124
+ tmpl_str = await self.get_template(name=template_name)
132
125
  text = text.replace("`", "\\`")
133
126
  return await self.render_custom_template(
134
127
  tmpl_str, {"text": text, "version": f"v{VERSION}"}, return_url
@@ -34,12 +34,18 @@ class HtmlRenderer:
34
34
  )
35
35
 
36
36
  async def render_t2i(
37
- self, text: str, use_network: bool = True, return_url: bool = False
37
+ self,
38
+ text: str,
39
+ use_network: bool = True,
40
+ return_url: bool = False,
41
+ template_name: str | None = None,
38
42
  ):
39
43
  """使用默认文转图模板。"""
40
44
  if use_network:
41
45
  try:
42
- return await self.network_strategy.render(text, return_url=return_url)
46
+ return await self.network_strategy.render(
47
+ text, return_url=return_url, template_name=template_name
48
+ )
43
49
  except BaseException as e:
44
50
  logger.error(
45
51
  f"Failed to render image via AstrBot API: {e}. Falling back to local rendering."
@@ -0,0 +1,184 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8"/>
5
+ <title>Astrbot PowerShell {{ version }} </title>
6
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.css" integrity="sha384-wcIxkf4k558AjM3Yz3BBFQUbk/zgIYC2R0QpeeYb+TwlBVMrlgLqwRjRtGZiK7ww" crossorigin="anonymous">
7
+ <script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/common.min.js"></script>
8
+ <script>hljs.highlightAll();</script>
9
+ <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js" integrity="sha384-hIoBPJpTUs74ddyc4bFZSM1TVlQDA60VBbJS0oA934VSz82sBx1X7kSx2ATBDIyd" crossorigin="anonymous"></script>
10
+ <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js" integrity="sha384-43gviWU0YVjaDtb/GhzOouOXtZMP/7XUzwPTstBeZFe/+rCMvRwr4yROQP43s0Xk" crossorigin="anonymous"
11
+ onload="renderMathInElement(document.getElementById('content'),{delimiters: [{left: '$$', right: '$$', display: true},{left: '$', right: '$', display: false}]});"></script>
12
+ <style>
13
+ :root {
14
+ --bg-color: #010409;
15
+ --text-color: #e6edf3;
16
+ --title-bar-color: #161b22;
17
+ --title-text-color: #e6edf3;
18
+ --font-family: 'Consolas', 'Microsoft YaHei Mono', 'Dengxian Mono', 'Courier New', monospace;
19
+ --glow-color: rgba(200, 220, 255, 0.7);
20
+ }
21
+
22
+ @keyframes scanline {
23
+ 0% {
24
+ background-position: 0 0;
25
+ }
26
+ 100% {
27
+ background-position: 0 100%;
28
+ }
29
+ }
30
+
31
+ body {
32
+ background-color: var(--bg-color);
33
+ color: var(--text-color);
34
+ font-family: var(--font-family);
35
+ margin: 0;
36
+ padding: 0;
37
+ line-height: 1.6;
38
+ font-size: 18px;
39
+ /* The CRT glow effect from the image */
40
+ text-shadow: 0 0 15px var(--glow-color), 0 0 7px rgba(255, 255, 255, 1);
41
+ position: relative;
42
+ overflow: hidden;
43
+ }
44
+
45
+ body::after {
46
+ content: " ";
47
+ display: block;
48
+ position: absolute;
49
+ top: 0;
50
+ left: 0;
51
+ right: 0;
52
+ bottom: 0;
53
+ background: linear-gradient(to bottom, transparent 50%, rgba(0, 0, 0, 0.3) 50%);
54
+ background-size: 100% 4px;
55
+ z-index: 2;
56
+ pointer-events: none;
57
+ animation: scanline 8s linear infinite;
58
+ }
59
+
60
+ .header {
61
+ background-color: var(--title-bar-color);
62
+ padding: 12px 18px;
63
+ color: var(--title-text-color);
64
+ font-size: 16px;
65
+ border-bottom: 1px solid #30363d;
66
+ text-shadow: none; /* No glow for title bar */
67
+ }
68
+
69
+ .header .title {
70
+ font-weight: bold;
71
+ font-size: 28px;
72
+ }
73
+
74
+ .header .version {
75
+ opacity: 0.8;
76
+ margin-left: 1rem;
77
+ }
78
+
79
+ main {
80
+ padding: 1rem 1.5rem;
81
+ }
82
+
83
+ #content {
84
+ /* min-width and max-width removed as per request */
85
+ }
86
+
87
+ /* --- Markdown Styles adjusted for terminal look --- */
88
+ h1, h2, h3, h4, h5, h6 {
89
+ line-height: 1.4;
90
+ margin-top: 20px;
91
+ margin-bottom: 10px;
92
+ padding-bottom: 5px;
93
+ border-bottom: 1px solid #30363d;
94
+ color: var(--text-color);
95
+ }
96
+ h1 { font-size: 2rem; }
97
+ h2 { font-size: 1.7rem; }
98
+ h3 { font-size: 1.4rem; }
99
+
100
+ p {
101
+ margin-top: 1rem;
102
+ margin-bottom: 1rem;
103
+ }
104
+
105
+ strong {
106
+ color: var(--text-color);
107
+ font-weight: bold;
108
+ }
109
+
110
+ img {
111
+ max-width: 100%;
112
+ border: 1px solid #30363d;
113
+ display: block;
114
+ margin: 1rem auto;
115
+ }
116
+
117
+ hr {
118
+ border: 0;
119
+ border-top: 1px dashed #30363d;
120
+ margin: 2rem 0;
121
+ }
122
+
123
+ code {
124
+ font-family: var(--font-family);
125
+ padding: 0.2em 0.4em;
126
+ margin: 0;
127
+ font-size: 90%;
128
+ background-color: #161b22;
129
+ border-radius: 4px;
130
+ }
131
+
132
+ pre {
133
+ font-family: var(--font-family);
134
+ border-radius: 4px;
135
+ background: #0d1117;
136
+ padding: 1rem;
137
+ overflow-x: auto;
138
+ border: 1px solid #30363d;
139
+ }
140
+
141
+ pre > code {
142
+ padding: 0;
143
+ margin: 0;
144
+ font-size: 100%;
145
+ background-color: transparent;
146
+ border-radius: 0;
147
+ text-shadow: none; /* Disable glow inside code blocks for clarity */
148
+ }
149
+
150
+ a {
151
+ color: #58a6ff;
152
+ text-decoration: underline;
153
+ }
154
+ a:hover {
155
+ text-decoration: underline;
156
+ }
157
+
158
+ blockquote {
159
+ border-left: 4px solid #30363d;
160
+ padding: 0.5rem 1rem;
161
+ margin: 1.5rem 0;
162
+ color: #8b949e;
163
+ background-color: #161b22;
164
+ }
165
+ </style>
166
+ </head>
167
+ <body>
168
+
169
+ <div class="header">
170
+ <span class="title">> Astrbot PowerShell</span>
171
+ <span class="version">{{ version }}</span>
172
+ </div>
173
+
174
+ <main>
175
+ <div id="content"></div>
176
+ </main>
177
+
178
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
179
+ <script>
180
+ document.getElementById('content').innerHTML = marked.parse(`{{ text | safe }}`);
181
+ </script>
182
+
183
+ </body>
184
+ </html>
@@ -0,0 +1,112 @@
1
+ # astrbot/core/utils/t2i/template_manager.py
2
+
3
+ import os
4
+ import shutil
5
+ from astrbot.core.utils.astrbot_path import get_astrbot_data_path, get_astrbot_path
6
+
7
+
8
+ class TemplateManager:
9
+ """
10
+ 负责管理 t2i HTML 模板的 CRUD 和重置操作。
11
+ 采用“用户覆盖内置”策略:用户模板存储在 data 目录中,并优先于内置模板加载。
12
+ 所有创建、更新、删除操作仅影响用户目录,以确保更新框架时用户数据安全。
13
+ """
14
+
15
+ CORE_TEMPLATES = ["base.html", "astrbot_powershell.html"]
16
+
17
+ def __init__(self):
18
+ self.builtin_template_dir = os.path.join(
19
+ get_astrbot_path(), "astrbot", "core", "utils", "t2i", "template"
20
+ )
21
+ self.user_template_dir = os.path.join(get_astrbot_data_path(), "t2i_templates")
22
+
23
+ os.makedirs(self.user_template_dir, exist_ok=True)
24
+ self._initialize_user_templates()
25
+
26
+ def _copy_core_templates(self, overwrite: bool = False):
27
+ """从内置目录复制核心模板到用户目录。"""
28
+ for filename in self.CORE_TEMPLATES:
29
+ src = os.path.join(self.builtin_template_dir, filename)
30
+ dst = os.path.join(self.user_template_dir, filename)
31
+ if os.path.exists(src) and (overwrite or not os.path.exists(dst)):
32
+ shutil.copyfile(src, dst)
33
+
34
+ def _initialize_user_templates(self):
35
+ """如果用户目录下缺少核心模板,则进行复制。"""
36
+ self._copy_core_templates(overwrite=False)
37
+
38
+ def _get_user_template_path(self, name: str) -> str:
39
+ """获取用户模板的完整路径,防止路径遍历漏洞。"""
40
+ if ".." in name or "/" in name or "\\" in name:
41
+ raise ValueError("模板名称包含非法字符。")
42
+ return os.path.join(self.user_template_dir, f"{name}.html")
43
+
44
+ def _read_file(self, path: str) -> str:
45
+ """读取文件内容。"""
46
+ with open(path, "r", encoding="utf-8") as f:
47
+ return f.read()
48
+
49
+ def list_templates(self) -> list[dict]:
50
+ """
51
+ 列出所有可用模板。
52
+ 该列表是内置模板和用户模板的合并视图,用户模板将覆盖同名的内置模板。
53
+ """
54
+ dirs_to_scan = [self.builtin_template_dir, self.user_template_dir]
55
+ all_names = {
56
+ os.path.splitext(f)[0]
57
+ for d in dirs_to_scan
58
+ for f in os.listdir(d)
59
+ if f.endswith(".html")
60
+ }
61
+ return [
62
+ {"name": name, "is_default": name == "base"} for name in sorted(all_names)
63
+ ]
64
+
65
+ def get_template(self, name: str) -> str:
66
+ """
67
+ 获取指定模板的内容。
68
+ 优先从用户目录加载,如果不存在则回退到内置目录。
69
+ """
70
+ user_path = self._get_user_template_path(name)
71
+ if os.path.exists(user_path):
72
+ return self._read_file(user_path)
73
+
74
+ builtin_path = os.path.join(self.builtin_template_dir, f"{name}.html")
75
+ if os.path.exists(builtin_path):
76
+ return self._read_file(builtin_path)
77
+
78
+ raise FileNotFoundError("模板不存在。")
79
+
80
+ def create_template(self, name: str, content: str):
81
+ """在用户目录中创建一个新的模板文件。"""
82
+ path = self._get_user_template_path(name)
83
+ if os.path.exists(path):
84
+ raise FileExistsError("同名模板已存在。")
85
+ with open(path, "w", encoding="utf-8") as f:
86
+ f.write(content)
87
+
88
+ def update_template(self, name: str, content: str):
89
+ """
90
+ 更新一个模板。此操作始终写入用户目录。
91
+ 如果更新的是一个内置模板,此操作实际上会在用户目录中创建一个修改后的副本,
92
+ 从而实现对内置模板的“覆盖”。
93
+ """
94
+ path = self._get_user_template_path(name)
95
+ with open(path, "w", encoding="utf-8") as f:
96
+ f.write(content)
97
+
98
+ def delete_template(self, name: str):
99
+ """
100
+ 仅删除用户目录中的模板文件。
101
+ 如果删除的是一个覆盖了内置模板的用户模板,这将有效地“恢复”到内置版本。
102
+ """
103
+ path = self._get_user_template_path(name)
104
+ if not os.path.exists(path):
105
+ raise FileNotFoundError("用户模板不存在,无法删除。")
106
+ os.remove(path)
107
+
108
+ def reset_default_template(self):
109
+ """
110
+ 将核心模板从内置目录强制重置到用户目录。
111
+ """
112
+ self._copy_core_templates(overwrite=True)
@@ -157,7 +157,11 @@ class ChatRoute(Route):
157
157
 
158
158
  if type == "end":
159
159
  break
160
- elif (streaming and type == "complete") or not streaming:
160
+ elif (
161
+ (streaming and type == "complete")
162
+ or not streaming
163
+ or type == "break"
164
+ ):
161
165
  # append bot message
162
166
  new_his = {"type": "bot", "message": result_text}
163
167
  await self.platform_history_mgr.insert(
@@ -197,6 +201,7 @@ class ChatRoute(Route):
197
201
  "Connection": "keep-alive",
198
202
  },
199
203
  )
204
+ response.timeout = None # fix SSE auto disconnect issue
200
205
  return response
201
206
 
202
207
  async def _get_webchat_conv_id_from_conv_id(self, conversation_id: str) -> str: