AstrBot 4.0.0b4__py3-none-any.whl → 4.1.0__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 (43) hide show
  1. astrbot/api/event/filter/__init__.py +2 -0
  2. astrbot/cli/utils/basic.py +12 -3
  3. astrbot/core/astrbot_config_mgr.py +16 -9
  4. astrbot/core/config/default.py +82 -4
  5. astrbot/core/initial_loader.py +4 -1
  6. astrbot/core/message/components.py +59 -50
  7. astrbot/core/pipeline/process_stage/method/llm_request.py +6 -2
  8. astrbot/core/pipeline/result_decorate/stage.py +5 -1
  9. astrbot/core/platform/manager.py +25 -3
  10. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +26 -14
  11. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +11 -4
  12. astrbot/core/platform/sources/satori/satori_adapter.py +482 -0
  13. astrbot/core/platform/sources/satori/satori_event.py +221 -0
  14. astrbot/core/platform/sources/telegram/tg_adapter.py +0 -1
  15. astrbot/core/provider/entities.py +17 -15
  16. astrbot/core/provider/sources/gemini_source.py +57 -18
  17. astrbot/core/provider/sources/openai_source.py +12 -5
  18. astrbot/core/provider/sources/vllm_rerank_source.py +6 -0
  19. astrbot/core/star/__init__.py +7 -5
  20. astrbot/core/star/filter/command.py +9 -3
  21. astrbot/core/star/filter/platform_adapter_type.py +3 -0
  22. astrbot/core/star/register/__init__.py +2 -0
  23. astrbot/core/star/register/star_handler.py +18 -4
  24. astrbot/core/star/star_handler.py +9 -1
  25. astrbot/core/star/star_tools.py +116 -21
  26. astrbot/core/updator.py +7 -5
  27. astrbot/core/utils/io.py +1 -1
  28. astrbot/core/utils/t2i/network_strategy.py +11 -18
  29. astrbot/core/utils/t2i/renderer.py +8 -2
  30. astrbot/core/utils/t2i/template/astrbot_powershell.html +184 -0
  31. astrbot/core/utils/t2i/template_manager.py +112 -0
  32. astrbot/core/zip_updator.py +26 -4
  33. astrbot/dashboard/routes/chat.py +6 -1
  34. astrbot/dashboard/routes/config.py +24 -49
  35. astrbot/dashboard/routes/route.py +19 -2
  36. astrbot/dashboard/routes/t2i.py +230 -0
  37. astrbot/dashboard/routes/update.py +3 -5
  38. astrbot/dashboard/server.py +13 -4
  39. {astrbot-4.0.0b4.dist-info → astrbot-4.1.0.dist-info}/METADATA +40 -53
  40. {astrbot-4.0.0b4.dist-info → astrbot-4.1.0.dist-info}/RECORD +43 -38
  41. {astrbot-4.0.0b4.dist-info → astrbot-4.1.0.dist-info}/WHEEL +0 -0
  42. {astrbot-4.0.0b4.dist-info → astrbot-4.1.0.dist-info}/entry_points.txt +0 -0
  43. {astrbot-4.0.0b4.dist-info → astrbot-4.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -107,16 +107,38 @@ class RepoZipUpdator:
107
107
  """Semver 版本比较"""
108
108
  return VersionComparator.compare_version(v1, v2)
109
109
 
110
- async def check_update(self, url: str, current_version: str) -> ReleaseInfo | None:
110
+ async def check_update(
111
+ self, url: str, current_version: str, consider_prerelease: bool = True
112
+ ) -> ReleaseInfo | None:
111
113
  update_data = await self.fetch_release_info(url)
112
- tag_name = update_data[0]["tag_name"]
114
+
115
+ sel_release_data = None
116
+ if consider_prerelease:
117
+ tag_name = update_data[0]["tag_name"]
118
+ sel_release_data = update_data[0]
119
+ else:
120
+ for data in update_data:
121
+ # 跳过带有 alpha、beta 等预发布标签的版本
122
+ if re.search(
123
+ r"[\-_.]?(alpha|beta|rc|dev)[\-_.]?\d*$",
124
+ data["tag_name"],
125
+ re.IGNORECASE,
126
+ ):
127
+ continue
128
+ tag_name = data["tag_name"]
129
+ sel_release_data = data
130
+ break
131
+
132
+ if not sel_release_data or not tag_name:
133
+ logger.error("未找到合适的发布版本")
134
+ return None
113
135
 
114
136
  if self.compare_version(current_version, tag_name) >= 0:
115
137
  return None
116
138
  return ReleaseInfo(
117
139
  version=tag_name,
118
- published_at=update_data[0]["published_at"],
119
- body=update_data[0]["body"],
140
+ published_at=sel_release_data["published_at"],
141
+ body=f"{tag_name}\n\n{sel_release_data['body']}",
120
142
  )
121
143
 
122
144
  async def download_from_repo_url(self, target_path: str, repo_url: str, proxy=""):
@@ -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:
@@ -1,6 +1,7 @@
1
1
  import typing
2
2
  import traceback
3
3
  import os
4
+ import copy
4
5
  from .route import Route, Response, RouteContext
5
6
  from astrbot.core.provider.entities import ProviderType
6
7
  from quart import request
@@ -16,10 +17,10 @@ from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
16
17
  from astrbot.core.platform.register import platform_registry
17
18
  from astrbot.core.provider.register import provider_registry
18
19
  from astrbot.core.star.star import star_registry
19
- from astrbot.core import logger, html_renderer
20
+ from astrbot.core import logger
20
21
  from astrbot.core.provider import Provider
22
+ from astrbot.core.provider.provider import RerankProvider
21
23
  import asyncio
22
- from astrbot.core.utils.t2i.network_strategy import CUSTOM_T2I_TEMPLATE_PATH
23
24
 
24
25
 
25
26
  def try_cast(value: str, type_: str):
@@ -155,6 +156,7 @@ def save_config(post_config: dict, config: AstrBotConfig, is_core: bool = False)
155
156
  raise ValueError(f"验证配置时出现异常: {e}")
156
157
  if errors:
157
158
  raise ValueError(f"格式校验未通过: {errors}")
159
+
158
160
  config.save_config(post_config)
159
161
 
160
162
 
@@ -185,56 +187,9 @@ class ConfigRoute(Route):
185
187
  "/config/provider/check_one": ("GET", self.check_one_provider_status),
186
188
  "/config/provider/list": ("GET", self.get_provider_config_list),
187
189
  "/config/provider/model_list": ("GET", self.get_provider_model_list),
188
- "/config/astrbot/t2i-template/get": ("GET", self.get_t2i_template),
189
- "/config/astrbot/t2i-template/save": ("POST", self.post_t2i_template),
190
- "/config/astrbot/t2i-template/delete": ("DELETE", self.delete_t2i_template),
191
190
  }
192
191
  self.register_routes()
193
192
 
194
- async def get_t2i_template(self):
195
- """获取 T2I 模板"""
196
- try:
197
- template = await html_renderer.network_strategy.get_template()
198
- has_custom_template = os.path.exists(CUSTOM_T2I_TEMPLATE_PATH)
199
- return (
200
- Response()
201
- .ok({"template": template, "has_custom_template": has_custom_template})
202
- .__dict__
203
- )
204
- except Exception as e:
205
- logger.error(traceback.format_exc())
206
- return Response().error(f"获取模板失败: {str(e)}").__dict__
207
-
208
- async def post_t2i_template(self):
209
- """保存 T2I 模板"""
210
- try:
211
- post_data = await request.json
212
- if not post_data or "template" not in post_data:
213
- return Response().error("缺少模板内容").__dict__
214
-
215
- template_content = post_data["template"]
216
-
217
- # 保存自定义模板到文件
218
- with open(CUSTOM_T2I_TEMPLATE_PATH, "w", encoding="utf-8") as f:
219
- f.write(template_content)
220
-
221
- return Response().ok(message="模板保存成功").__dict__
222
- except Exception as e:
223
- logger.error(traceback.format_exc())
224
- return Response().error(f"保存模板失败: {str(e)}").__dict__
225
-
226
- async def delete_t2i_template(self):
227
- """删除自定义 T2I 模板,恢复默认模板"""
228
- try:
229
- if os.path.exists(CUSTOM_T2I_TEMPLATE_PATH):
230
- os.remove(CUSTOM_T2I_TEMPLATE_PATH)
231
- return Response().ok(message="已恢复默认模板").__dict__
232
- else:
233
- return Response().ok(message="未找到自定义模板文件").__dict__
234
- except Exception as e:
235
- logger.error(traceback.format_exc())
236
- return Response().error(f"删除模板失败: {str(e)}").__dict__
237
-
238
193
  async def get_abconf_list(self):
239
194
  """获取所有 AstrBot 配置文件的列表"""
240
195
  abconf_list = self.acm.get_conf_list()
@@ -481,6 +436,19 @@ class ConfigRoute(Route):
481
436
  )
482
437
  status_info["status"] = "unavailable"
483
438
  status_info["error"] = f"STT test failed: {str(e)}"
439
+ elif provider_capability_type == ProviderType.RERANK:
440
+ try:
441
+ assert isinstance(provider, RerankProvider)
442
+ await provider.rerank("Apple", documents=["apple", "banana"])
443
+ status_info["status"] = "available"
444
+ except Exception as e:
445
+ logger.error(
446
+ f"Error testing rerank provider {provider_name}: {e}",
447
+ exc_info=True,
448
+ )
449
+ status_info["status"] = "unavailable"
450
+ status_info["error"] = f"Rerank test failed: {str(e)}"
451
+
484
452
  else:
485
453
  logger.debug(
486
454
  f"Provider {provider_name} is not a Chat Completion or Embedding provider. Marking as available without test. Meta: {meta}"
@@ -752,6 +720,13 @@ class ConfigRoute(Route):
752
720
  if conf_id not in self.acm.confs:
753
721
  raise ValueError(f"配置文件 {conf_id} 不存在")
754
722
  astrbot_config = self.acm.confs[conf_id]
723
+
724
+ # 保留服务端的 t2i_active_template 值
725
+ if "t2i_active_template" in astrbot_config:
726
+ post_configs["t2i_active_template"] = astrbot_config[
727
+ "t2i_active_template"
728
+ ]
729
+
755
730
  save_config(post_configs, astrbot_config, is_core=True)
756
731
  except Exception as e:
757
732
  raise e
@@ -1,3 +1,4 @@
1
+ from astrbot.core import logger
1
2
  from astrbot.core.config.astrbot_config import AstrBotConfig
2
3
  from dataclasses import dataclass
3
4
  from quart import Quart
@@ -15,8 +16,24 @@ class Route:
15
16
  self.config = context.config
16
17
 
17
18
  def register_routes(self):
18
- for route, (method, func) in self.routes.items():
19
- self.app.add_url_rule(f"/api{route}", view_func=func, methods=[method])
19
+ def _add_rule(path, method, func):
20
+ # 统一添加 /api 前缀
21
+ full_path = f"/api{path}"
22
+ self.app.add_url_rule(full_path, view_func=func, methods=[method])
23
+
24
+ # 兼容字典和列表两种格式
25
+ routes_to_register = (
26
+ self.routes.items() if isinstance(self.routes, dict) else self.routes
27
+ )
28
+
29
+ for route, definition in routes_to_register:
30
+ # 兼容一个路由多个方法
31
+ if isinstance(definition, list):
32
+ for method, func in definition:
33
+ _add_rule(route, method, func)
34
+ else:
35
+ method, func = definition
36
+ _add_rule(route, method, func)
20
37
 
21
38
 
22
39
  @dataclass
@@ -0,0 +1,230 @@
1
+ # astrbot/dashboard/routes/t2i.py
2
+
3
+ from dataclasses import asdict
4
+ from quart import jsonify, request
5
+
6
+ from astrbot.core import logger
7
+ from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
8
+ from astrbot.core.utils.t2i.template_manager import TemplateManager
9
+ from .route import Response, Route, RouteContext
10
+
11
+
12
+ class T2iRoute(Route):
13
+ def __init__(self, context: RouteContext, core_lifecycle: AstrBotCoreLifecycle):
14
+ super().__init__(context)
15
+ self.core_lifecycle = core_lifecycle
16
+ self.config = core_lifecycle.astrbot_config
17
+ self.manager = TemplateManager()
18
+ # 使用列表保证路由注册顺序,避免 /<name> 路由优先匹配 /reset_default
19
+ self.routes = [
20
+ ("/t2i/templates", ("GET", self.list_templates)),
21
+ ("/t2i/templates/active", ("GET", self.get_active_template)),
22
+ ("/t2i/templates/create", ("POST", self.create_template)),
23
+ ("/t2i/templates/reset_default", ("POST", self.reset_default_template)),
24
+ ("/t2i/templates/set_active", ("POST", self.set_active_template)),
25
+ # 动态路由应该在静态路由之后注册
26
+ (
27
+ "/t2i/templates/<name>",
28
+ [
29
+ ("GET", self.get_template),
30
+ ("PUT", self.update_template),
31
+ ("DELETE", self.delete_template),
32
+ ],
33
+ ),
34
+ ]
35
+ self.register_routes()
36
+
37
+ async def list_templates(self):
38
+ """获取所有T2I模板列表"""
39
+ try:
40
+ templates = self.manager.list_templates()
41
+ return jsonify(asdict(Response().ok(data=templates)))
42
+ except Exception as e:
43
+ response = jsonify(asdict(Response().error(str(e))))
44
+ response.status_code = 500
45
+ return response
46
+
47
+ async def get_active_template(self):
48
+ """获取当前激活的T2I模板"""
49
+ try:
50
+ active_template = self.config.get("t2i_active_template", "base")
51
+ return jsonify(
52
+ asdict(Response().ok(data={"active_template": active_template}))
53
+ )
54
+ except Exception as e:
55
+ logger.error("Error in get_active_template", exc_info=True)
56
+ response = jsonify(asdict(Response().error(str(e))))
57
+ response.status_code = 500
58
+ return response
59
+
60
+ async def get_template(self, name: str):
61
+ """获取指定名称的T2I模板内容"""
62
+ try:
63
+ content = self.manager.get_template(name)
64
+ return jsonify(
65
+ asdict(Response().ok(data={"name": name, "content": content}))
66
+ )
67
+ except FileNotFoundError:
68
+ response = jsonify(asdict(Response().error("Template not found")))
69
+ response.status_code = 404
70
+ return response
71
+ except Exception as e:
72
+ response = jsonify(asdict(Response().error(str(e))))
73
+ response.status_code = 500
74
+ return response
75
+
76
+ async def create_template(self):
77
+ """创建一个新的T2I模板"""
78
+ try:
79
+ data = await request.json
80
+ name = data.get("name")
81
+ content = data.get("content")
82
+ if not name or not content:
83
+ response = jsonify(
84
+ asdict(Response().error("Name and content are required."))
85
+ )
86
+ response.status_code = 400
87
+ return response
88
+ name = name.strip()
89
+
90
+ self.manager.create_template(name, content)
91
+ response = jsonify(
92
+ asdict(
93
+ Response().ok(
94
+ data={"name": name}, message="Template created successfully."
95
+ )
96
+ )
97
+ )
98
+ response.status_code = 201
99
+ return response
100
+ except FileExistsError:
101
+ response = jsonify(
102
+ asdict(Response().error("Template with this name already exists."))
103
+ )
104
+ response.status_code = 409
105
+ return response
106
+ except ValueError as e:
107
+ response = jsonify(asdict(Response().error(str(e))))
108
+ response.status_code = 400
109
+ return response
110
+ except Exception as e:
111
+ response = jsonify(asdict(Response().error(str(e))))
112
+ response.status_code = 500
113
+ return response
114
+
115
+ async def update_template(self, name: str):
116
+ """更新一个已存在的T2I模板"""
117
+ try:
118
+ name = name.strip()
119
+ data = await request.json
120
+ content = data.get("content")
121
+ if content is None:
122
+ response = jsonify(asdict(Response().error("Content is required.")))
123
+ response.status_code = 400
124
+ return response
125
+
126
+ self.manager.update_template(name, content)
127
+
128
+ # 检查更新的是否为当前激活的模板,如果是,则热重载
129
+ active_template = self.config.get("t2i_active_template", "base")
130
+ if name == active_template:
131
+ await self.core_lifecycle.reload_pipeline_scheduler("default")
132
+ message = f"模板 '{name}' 已更新并重新加载。"
133
+ else:
134
+ message = f"模板 '{name}' 已更新。"
135
+
136
+ return jsonify(asdict(Response().ok(data={"name": name}, message=message)))
137
+ except ValueError as e:
138
+ response = jsonify(asdict(Response().error(str(e))))
139
+ response.status_code = 400
140
+ return response
141
+ except Exception as e:
142
+ response = jsonify(asdict(Response().error(str(e))))
143
+ response.status_code = 500
144
+ return response
145
+
146
+ async def delete_template(self, name: str):
147
+ """删除一个T2I模板"""
148
+ try:
149
+ name = name.strip()
150
+ self.manager.delete_template(name)
151
+ return jsonify(
152
+ asdict(Response().ok(message="Template deleted successfully."))
153
+ )
154
+ except FileNotFoundError:
155
+ response = jsonify(asdict(Response().error("Template not found.")))
156
+ response.status_code = 404
157
+ return response
158
+ except ValueError as e:
159
+ response = jsonify(asdict(Response().error(str(e))))
160
+ response.status_code = 400
161
+ return response
162
+ except Exception as e:
163
+ response = jsonify(asdict(Response().error(str(e))))
164
+ response.status_code = 500
165
+ return response
166
+
167
+ async def set_active_template(self):
168
+ """设置当前活动的T2I模板"""
169
+ try:
170
+ data = await request.json
171
+ name = data.get("name")
172
+ if not name:
173
+ response = jsonify(asdict(Response().error("模板名称(name)不能为空。")))
174
+ response.status_code = 400
175
+ return response
176
+
177
+ # 验证模板文件是否存在
178
+ self.manager.get_template(name)
179
+
180
+ # 更新配置
181
+ config = self.config
182
+ config["t2i_active_template"] = name
183
+ config.save_config(config)
184
+
185
+ # 热重载以应用更改
186
+ await self.core_lifecycle.reload_pipeline_scheduler("default")
187
+
188
+ return jsonify(asdict(Response().ok(message=f"模板 '{name}' 已成功应用。")))
189
+
190
+ except FileNotFoundError:
191
+ response = jsonify(
192
+ asdict(Response().error(f"模板 '{name}' 不存在,无法应用。"))
193
+ )
194
+ response.status_code = 404
195
+ return response
196
+ except Exception as e:
197
+ logger.error("Error in set_active_template", exc_info=True)
198
+ response = jsonify(asdict(Response().error(str(e))))
199
+ response.status_code = 500
200
+ return response
201
+
202
+ async def reset_default_template(self):
203
+ """重置默认的'base'模板"""
204
+ try:
205
+ self.manager.reset_default_template()
206
+
207
+ # 更新配置,将激活模板也重置为'base'
208
+ config = self.config
209
+ config["t2i_active_template"] = "base"
210
+ config.save_config(config)
211
+
212
+ # 热重载以应用更改
213
+ await self.core_lifecycle.reload_pipeline_scheduler("default")
214
+
215
+ return jsonify(
216
+ asdict(
217
+ Response().ok(
218
+ message="Default template has been reset and activated."
219
+ )
220
+ )
221
+ )
222
+ except FileNotFoundError as e:
223
+ response = jsonify(asdict(Response().error(str(e))))
224
+ response.status_code = 404
225
+ return response
226
+ except Exception as e:
227
+ logger.error("Error in reset_default_template", exc_info=True)
228
+ response = jsonify(asdict(Response().error(str(e))))
229
+ response.status_code = 500
230
+ return response
@@ -57,7 +57,7 @@ class UpdateRoute(Route):
57
57
  .__dict__
58
58
  )
59
59
  else:
60
- ret = await self.astrbot_updator.check_update(None, None)
60
+ ret = await self.astrbot_updator.check_update(None, None, False)
61
61
  return Response(
62
62
  status="success",
63
63
  message=str(ret) if ret is not None else "已经是最新版本了。",
@@ -100,9 +100,7 @@ class UpdateRoute(Route):
100
100
  )
101
101
 
102
102
  try:
103
- await download_dashboard(
104
- latest=latest, version=version, proxy=proxy
105
- )
103
+ await download_dashboard(latest=latest, version=version, proxy=proxy)
106
104
  except Exception as e:
107
105
  logger.error(f"下载管理面板文件失败: {e}。")
108
106
 
@@ -133,7 +131,7 @@ class UpdateRoute(Route):
133
131
  async def update_dashboard(self):
134
132
  try:
135
133
  try:
136
- await download_dashboard()
134
+ await download_dashboard(version=f"v{VERSION}", latest=False)
137
135
  except Exception as e:
138
136
  logger.error(f"下载管理面板文件失败: {e}。")
139
137
  return Response().error(f"下载管理面板文件失败: {e}").__dict__
@@ -18,6 +18,7 @@ from astrbot.core.utils.io import get_local_ip_addresses
18
18
  from .routes import *
19
19
  from .routes.route import Response, RouteContext
20
20
  from .routes.session_management import SessionManagementRoute
21
+ from .routes.t2i import T2iRoute
21
22
 
22
23
  APP: Quart = None
23
24
 
@@ -28,10 +29,19 @@ class AstrBotDashboard:
28
29
  core_lifecycle: AstrBotCoreLifecycle,
29
30
  db: BaseDatabase,
30
31
  shutdown_event: asyncio.Event,
32
+ webui_dir: str | None = None,
31
33
  ) -> None:
32
34
  self.core_lifecycle = core_lifecycle
33
35
  self.config = core_lifecycle.astrbot_config
34
- self.data_path = os.path.abspath(os.path.join(get_astrbot_data_path(), "dist"))
36
+
37
+ # 参数指定webui目录
38
+ if webui_dir and os.path.exists(webui_dir):
39
+ self.data_path = os.path.abspath(webui_dir)
40
+ else:
41
+ self.data_path = os.path.abspath(
42
+ os.path.join(get_astrbot_data_path(), "dist")
43
+ )
44
+
35
45
  self.app = Quart("dashboard", static_folder=self.data_path, static_url_path="/")
36
46
  APP = self.app # noqa
37
47
  self.app.config["MAX_CONTENT_LENGTH"] = (
@@ -60,9 +70,8 @@ class AstrBotDashboard:
60
70
  self.session_management_route = SessionManagementRoute(
61
71
  self.context, db, core_lifecycle
62
72
  )
63
- self.persona_route = PersonaRoute(
64
- self.context, db, core_lifecycle
65
- )
73
+ self.persona_route = PersonaRoute(self.context, db, core_lifecycle)
74
+ self.t2i_route = T2iRoute(self.context, core_lifecycle)
66
75
 
67
76
  self.app.add_url_rule(
68
77
  "/api/plug/<path:subpath>",