AstrBot 4.13.2__py3-none-any.whl → 4.14.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 (61) hide show
  1. astrbot/builtin_stars/astrbot/main.py +0 -6
  2. astrbot/builtin_stars/session_controller/main.py +1 -2
  3. astrbot/cli/__init__.py +1 -1
  4. astrbot/core/agent/agent.py +2 -1
  5. astrbot/core/agent/handoff.py +14 -1
  6. astrbot/core/agent/runners/tool_loop_agent_runner.py +14 -1
  7. astrbot/core/agent/tool.py +5 -0
  8. astrbot/core/astr_agent_run_util.py +21 -3
  9. astrbot/core/astr_agent_tool_exec.py +178 -3
  10. astrbot/core/astr_main_agent.py +980 -0
  11. astrbot/core/astr_main_agent_resources.py +453 -0
  12. astrbot/core/computer/computer_client.py +10 -1
  13. astrbot/core/computer/tools/fs.py +22 -14
  14. astrbot/core/config/default.py +84 -58
  15. astrbot/core/core_lifecycle.py +43 -1
  16. astrbot/core/cron/__init__.py +3 -0
  17. astrbot/core/cron/events.py +67 -0
  18. astrbot/core/cron/manager.py +376 -0
  19. astrbot/core/db/__init__.py +60 -0
  20. astrbot/core/db/po.py +31 -0
  21. astrbot/core/db/sqlite.py +120 -0
  22. astrbot/core/event_bus.py +0 -1
  23. astrbot/core/message/message_event_result.py +21 -3
  24. astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +111 -580
  25. astrbot/core/pipeline/scheduler.py +0 -2
  26. astrbot/core/platform/astr_message_event.py +5 -5
  27. astrbot/core/platform/platform.py +9 -0
  28. astrbot/core/platform/platform_metadata.py +2 -0
  29. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +1 -0
  30. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +1 -0
  31. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +1 -0
  32. astrbot/core/platform/sources/webchat/webchat_adapter.py +1 -0
  33. astrbot/core/platform/sources/wecom/wecom_adapter.py +1 -0
  34. astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +1 -0
  35. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +1 -0
  36. astrbot/core/provider/entities.py +1 -1
  37. astrbot/core/skills/skill_manager.py +9 -8
  38. astrbot/core/star/context.py +8 -0
  39. astrbot/core/star/filter/custom_filter.py +3 -3
  40. astrbot/core/star/register/star_handler.py +1 -1
  41. astrbot/core/subagent_orchestrator.py +96 -0
  42. astrbot/core/tools/cron_tools.py +174 -0
  43. astrbot/core/utils/history_saver.py +31 -0
  44. astrbot/core/utils/trace.py +4 -0
  45. astrbot/dashboard/routes/__init__.py +4 -0
  46. astrbot/dashboard/routes/cron.py +174 -0
  47. astrbot/dashboard/routes/log.py +36 -0
  48. astrbot/dashboard/routes/plugin.py +11 -0
  49. astrbot/dashboard/routes/skills.py +12 -37
  50. astrbot/dashboard/routes/subagent.py +117 -0
  51. astrbot/dashboard/routes/tools.py +41 -14
  52. astrbot/dashboard/server.py +3 -0
  53. {astrbot-4.13.2.dist-info → astrbot-4.14.1.dist-info}/METADATA +21 -2
  54. {astrbot-4.13.2.dist-info → astrbot-4.14.1.dist-info}/RECORD +57 -51
  55. astrbot/builtin_stars/astrbot/process_llm_request.py +0 -308
  56. astrbot/builtin_stars/reminder/main.py +0 -266
  57. astrbot/builtin_stars/reminder/metadata.yaml +0 -4
  58. astrbot/core/pipeline/process_stage/utils.py +0 -219
  59. {astrbot-4.13.2.dist-info → astrbot-4.14.1.dist-info}/WHEEL +0 -0
  60. {astrbot-4.13.2.dist-info → astrbot-4.14.1.dist-info}/entry_points.txt +0 -0
  61. {astrbot-4.13.2.dist-info → astrbot-4.14.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,174 @@
1
+ import traceback
2
+ from datetime import datetime
3
+
4
+ from quart import jsonify, request
5
+
6
+ from astrbot.core import logger
7
+ from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
8
+
9
+ from .route import Response, Route, RouteContext
10
+
11
+
12
+ class CronRoute(Route):
13
+ def __init__(
14
+ self, context: RouteContext, core_lifecycle: AstrBotCoreLifecycle
15
+ ) -> None:
16
+ super().__init__(context)
17
+ self.core_lifecycle = core_lifecycle
18
+ self.routes = [
19
+ ("/cron/jobs", ("GET", self.list_jobs)),
20
+ ("/cron/jobs", ("POST", self.create_job)),
21
+ ("/cron/jobs/<job_id>", ("PATCH", self.update_job)),
22
+ ("/cron/jobs/<job_id>", ("DELETE", self.delete_job)),
23
+ ]
24
+ self.register_routes()
25
+
26
+ def _serialize_job(self, job):
27
+ data = job.model_dump() if hasattr(job, "model_dump") else job.__dict__
28
+ for k in ["created_at", "updated_at", "last_run_at", "next_run_time"]:
29
+ if isinstance(data.get(k), datetime):
30
+ data[k] = data[k].isoformat()
31
+ # expose note explicitly for UI (prefer payload.note then description)
32
+ payload = data.get("payload") or {}
33
+ data["note"] = payload.get("note") or data.get("description") or ""
34
+ data["run_at"] = payload.get("run_at")
35
+ data["run_once"] = data.get("run_once", False)
36
+ # status is internal; hide to avoid implying one-time completion for recurring jobs
37
+ data.pop("status", None)
38
+ return data
39
+
40
+ async def list_jobs(self):
41
+ try:
42
+ cron_mgr = self.core_lifecycle.cron_manager
43
+ if cron_mgr is None:
44
+ return jsonify(
45
+ Response().error("Cron manager not initialized").__dict__
46
+ )
47
+ job_type = request.args.get("type")
48
+ jobs = await cron_mgr.list_jobs(job_type)
49
+ data = [self._serialize_job(j) for j in jobs]
50
+ return jsonify(Response().ok(data=data).__dict__)
51
+ except Exception as e: # noqa: BLE001
52
+ logger.error(traceback.format_exc())
53
+ return jsonify(Response().error(f"Failed to list jobs: {e!s}").__dict__)
54
+
55
+ async def create_job(self):
56
+ try:
57
+ cron_mgr = self.core_lifecycle.cron_manager
58
+ if cron_mgr is None:
59
+ return jsonify(
60
+ Response().error("Cron manager not initialized").__dict__
61
+ )
62
+
63
+ payload = await request.json
64
+ if not isinstance(payload, dict):
65
+ return jsonify(Response().error("Invalid payload").__dict__)
66
+
67
+ name = payload.get("name") or "active_agent_task"
68
+ cron_expression = payload.get("cron_expression")
69
+ note = payload.get("note") or payload.get("description") or name
70
+ session = payload.get("session")
71
+ persona_id = payload.get("persona_id")
72
+ provider_id = payload.get("provider_id")
73
+ timezone = payload.get("timezone")
74
+ enabled = bool(payload.get("enabled", True))
75
+ run_once = bool(payload.get("run_once", False))
76
+ run_at = payload.get("run_at")
77
+
78
+ if not session:
79
+ return jsonify(Response().error("session is required").__dict__)
80
+ if run_once and not run_at:
81
+ return jsonify(
82
+ Response().error("run_at is required when run_once=true").__dict__
83
+ )
84
+ if (not run_once) and not cron_expression:
85
+ return jsonify(
86
+ Response()
87
+ .error("cron_expression is required when run_once=false")
88
+ .__dict__
89
+ )
90
+ if run_once and cron_expression:
91
+ cron_expression = None # ignore cron when run_once specified
92
+ run_at_dt = None
93
+ if run_at:
94
+ try:
95
+ run_at_dt = datetime.fromisoformat(str(run_at))
96
+ except Exception:
97
+ return jsonify(
98
+ Response().error("run_at must be ISO datetime").__dict__
99
+ )
100
+
101
+ job_payload = {
102
+ "session": session,
103
+ "note": note,
104
+ "persona_id": persona_id,
105
+ "provider_id": provider_id,
106
+ "run_at": run_at,
107
+ "origin": "api",
108
+ }
109
+
110
+ job = await cron_mgr.add_active_job(
111
+ name=name,
112
+ cron_expression=cron_expression,
113
+ payload=job_payload,
114
+ description=note,
115
+ timezone=timezone,
116
+ enabled=enabled,
117
+ run_once=run_once,
118
+ run_at=run_at_dt,
119
+ )
120
+
121
+ return jsonify(Response().ok(data=self._serialize_job(job)).__dict__)
122
+ except Exception as e: # noqa: BLE001
123
+ logger.error(traceback.format_exc())
124
+ return jsonify(Response().error(f"Failed to create job: {e!s}").__dict__)
125
+
126
+ async def update_job(self, job_id: str):
127
+ try:
128
+ cron_mgr = self.core_lifecycle.cron_manager
129
+ if cron_mgr is None:
130
+ return jsonify(
131
+ Response().error("Cron manager not initialized").__dict__
132
+ )
133
+
134
+ payload = await request.json
135
+ if not isinstance(payload, dict):
136
+ return jsonify(Response().error("Invalid payload").__dict__)
137
+
138
+ updates = {
139
+ "name": payload.get("name"),
140
+ "cron_expression": payload.get("cron_expression"),
141
+ "description": payload.get("description"),
142
+ "enabled": payload.get("enabled"),
143
+ "timezone": payload.get("timezone"),
144
+ "run_once": payload.get("run_once"),
145
+ "payload": payload.get("payload"),
146
+ }
147
+ # remove None values to avoid unwanted resets
148
+ updates = {k: v for k, v in updates.items() if v is not None}
149
+ if "run_at" in payload:
150
+ updates.setdefault("payload", {})
151
+ if updates["payload"] is None:
152
+ updates["payload"] = {}
153
+ updates["payload"]["run_at"] = payload.get("run_at")
154
+
155
+ job = await cron_mgr.update_job(job_id, **updates)
156
+ if not job:
157
+ return jsonify(Response().error("Job not found").__dict__)
158
+ return jsonify(Response().ok(data=self._serialize_job(job)).__dict__)
159
+ except Exception as e: # noqa: BLE001
160
+ logger.error(traceback.format_exc())
161
+ return jsonify(Response().error(f"Failed to update job: {e!s}").__dict__)
162
+
163
+ async def delete_job(self, job_id: str):
164
+ try:
165
+ cron_mgr = self.core_lifecycle.cron_manager
166
+ if cron_mgr is None:
167
+ return jsonify(
168
+ Response().error("Cron manager not initialized").__dict__
169
+ )
170
+ await cron_mgr.delete_job(job_id)
171
+ return jsonify(Response().ok(message="deleted").__dict__)
172
+ except Exception as e: # noqa: BLE001
173
+ logger.error(traceback.format_exc())
174
+ return jsonify(Response().error(f"Failed to delete job: {e!s}").__dict__)
@@ -31,6 +31,16 @@ class LogRoute(Route):
31
31
  view_func=self.log_history,
32
32
  methods=["GET"],
33
33
  )
34
+ self.app.add_url_rule(
35
+ "/api/trace/settings",
36
+ view_func=self.get_trace_settings,
37
+ methods=["GET"],
38
+ )
39
+ self.app.add_url_rule(
40
+ "/api/trace/settings",
41
+ view_func=self.update_trace_settings,
42
+ methods=["POST"],
43
+ )
34
44
 
35
45
  async def _replay_cached_logs(
36
46
  self, last_event_id: str
@@ -106,3 +116,29 @@ class LogRoute(Route):
106
116
  except Exception as e:
107
117
  logger.error(f"获取日志历史失败: {e}")
108
118
  return Response().error(f"获取日志历史失败: {e}").__dict__
119
+
120
+ async def get_trace_settings(self):
121
+ """获取 Trace 设置"""
122
+ try:
123
+ trace_enable = self.config.get("trace_enable", True)
124
+ return Response().ok(data={"trace_enable": trace_enable}).__dict__
125
+ except Exception as e:
126
+ logger.error(f"获取 Trace 设置失败: {e}")
127
+ return Response().error(f"获取 Trace 设置失败: {e}").__dict__
128
+
129
+ async def update_trace_settings(self):
130
+ """更新 Trace 设置"""
131
+ try:
132
+ data = await request.json
133
+ if data is None:
134
+ return Response().error("请求数据为空").__dict__
135
+
136
+ trace_enable = data.get("trace_enable")
137
+ if trace_enable is not None:
138
+ self.config["trace_enable"] = bool(trace_enable)
139
+ self.config.save_config()
140
+
141
+ return Response().ok(message="Trace 设置已更新").__dict__
142
+ except Exception as e:
143
+ logger.error(f"更新 Trace 设置失败: {e}")
144
+ return Response().error(f"更新 Trace 设置失败: {e}").__dict__
@@ -315,6 +315,17 @@ class PluginRoute(Route):
315
315
  "display_name": plugin.display_name,
316
316
  "logo": f"/api/file/{logo_url}" if logo_url else None,
317
317
  }
318
+ # 检查是否为全空的幽灵插件
319
+ if not any(
320
+ [
321
+ plugin.name,
322
+ plugin.author,
323
+ plugin.desc,
324
+ plugin.version,
325
+ plugin.display_name,
326
+ ]
327
+ ):
328
+ continue
318
329
  _plugin_resp.append(_t)
319
330
  return (
320
331
  Response()
@@ -4,7 +4,6 @@ import traceback
4
4
  from quart import request
5
5
 
6
6
  from astrbot.core import DEMO_MODE, logger
7
- from astrbot.core.computer.computer_client import get_booter
8
7
  from astrbot.core.skills.skill_manager import SkillManager
9
8
  from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
10
9
 
@@ -25,14 +24,22 @@ class SkillsRoute(Route):
25
24
 
26
25
  async def get_skills(self):
27
26
  try:
28
- cfg = self.core_lifecycle.astrbot_config.get("provider_settings", {}).get(
29
- "skills", {}
27
+ provider_settings = self.core_lifecycle.astrbot_config.get(
28
+ "provider_settings", {}
30
29
  )
31
- runtime = cfg.get("runtime", "local")
30
+ runtime = provider_settings.get("computer_use_runtime", "local")
32
31
  skills = SkillManager().list_skills(
33
32
  active_only=False, runtime=runtime, show_sandbox_path=False
34
33
  )
35
- return Response().ok([skill.__dict__ for skill in skills]).__dict__
34
+ return (
35
+ Response()
36
+ .ok(
37
+ {
38
+ "skills": [skill.__dict__ for skill in skills],
39
+ }
40
+ )
41
+ .__dict__
42
+ )
36
43
  except Exception as e:
37
44
  logger.error(traceback.format_exc())
38
45
  return Response().error(str(e)).__dict__
@@ -60,41 +67,9 @@ class SkillsRoute(Route):
60
67
  temp_path = os.path.join(temp_dir, filename)
61
68
  await file.save(temp_path)
62
69
 
63
- cfg = self.core_lifecycle.astrbot_config.get("provider_settings", {}).get(
64
- "skills", {}
65
- )
66
- runtime = cfg.get("runtime", "local")
67
- if runtime == "sandbox":
68
- sandbox_enabled = (
69
- self.core_lifecycle.astrbot_config.get("provider_settings", {})
70
- .get("sandbox", {})
71
- .get("enable", False)
72
- )
73
- if not sandbox_enabled:
74
- return (
75
- Response()
76
- .error(
77
- "Sandbox is not enabled. Please enable sandbox before using sandbox runtime."
78
- )
79
- .__dict__
80
- )
81
70
  skill_mgr = SkillManager()
82
71
  skill_name = skill_mgr.install_skill_from_zip(temp_path, overwrite=True)
83
72
 
84
- if runtime == "sandbox":
85
- sb = await get_booter(self.core_lifecycle.star_context, "skills-upload")
86
- remote_root = "/home/shared/skills"
87
- remote_zip = f"{remote_root}/{skill_name}.zip"
88
- await sb.shell.exec(f"mkdir -p {remote_root}")
89
- upload_result = await sb.upload_file(temp_path, remote_zip)
90
- if not upload_result.get("success", False):
91
- return (
92
- Response().error("Failed to upload skill to sandbox").__dict__
93
- )
94
- await sb.shell.exec(
95
- f"unzip -o {remote_zip} -d {remote_root} && rm -f {remote_zip}"
96
- )
97
-
98
73
  return (
99
74
  Response()
100
75
  .ok({"name": skill_name}, "Skill uploaded successfully.")
@@ -0,0 +1,117 @@
1
+ import traceback
2
+
3
+ from quart import jsonify, request
4
+
5
+ from astrbot.core import logger
6
+ from astrbot.core.agent.handoff import HandoffTool
7
+ from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
8
+
9
+ from .route import Response, Route, RouteContext
10
+
11
+
12
+ class SubAgentRoute(Route):
13
+ def __init__(
14
+ self,
15
+ context: RouteContext,
16
+ core_lifecycle: AstrBotCoreLifecycle,
17
+ ) -> None:
18
+ super().__init__(context)
19
+ self.core_lifecycle = core_lifecycle
20
+ # NOTE: dict cannot hold duplicate keys; use list form to register multiple
21
+ # methods for the same path.
22
+ self.routes = [
23
+ ("/subagent/config", ("GET", self.get_config)),
24
+ ("/subagent/config", ("POST", self.update_config)),
25
+ ("/subagent/available-tools", ("GET", self.get_available_tools)),
26
+ ]
27
+ self.register_routes()
28
+
29
+ async def get_config(self):
30
+ try:
31
+ cfg = self.core_lifecycle.astrbot_config
32
+ data = cfg.get("subagent_orchestrator")
33
+
34
+ # First-time access: return a sane default instead of erroring.
35
+ if not isinstance(data, dict):
36
+ data = {
37
+ "main_enable": False,
38
+ "remove_main_duplicate_tools": False,
39
+ "agents": [],
40
+ }
41
+
42
+ # Backward compatibility: older config used `enable`.
43
+ if (
44
+ isinstance(data, dict)
45
+ and "main_enable" not in data
46
+ and "enable" in data
47
+ ):
48
+ data["main_enable"] = bool(data.get("enable", False))
49
+
50
+ # Ensure required keys exist.
51
+ data.setdefault("main_enable", False)
52
+ data.setdefault("remove_main_duplicate_tools", False)
53
+ data.setdefault("agents", [])
54
+
55
+ # Backward/forward compatibility: ensure each agent contains provider_id.
56
+ # None means follow global/default provider settings.
57
+ if isinstance(data.get("agents"), list):
58
+ for a in data["agents"]:
59
+ if isinstance(a, dict):
60
+ a.setdefault("provider_id", None)
61
+ a.setdefault("persona_id", None)
62
+ return jsonify(Response().ok(data=data).__dict__)
63
+ except Exception as e:
64
+ logger.error(traceback.format_exc())
65
+ return jsonify(Response().error(f"获取 subagent 配置失败: {e!s}").__dict__)
66
+
67
+ async def update_config(self):
68
+ try:
69
+ data = await request.json
70
+ if not isinstance(data, dict):
71
+ return jsonify(Response().error("配置必须为 JSON 对象").__dict__)
72
+
73
+ cfg = self.core_lifecycle.astrbot_config
74
+ cfg["subagent_orchestrator"] = data
75
+
76
+ # Persist to cmd_config.json
77
+ # AstrBotConfigManager does not expose a `save()` method; persist via AstrBotConfig.
78
+ cfg.save_config()
79
+
80
+ # Reload dynamic handoff tools if orchestrator exists
81
+ orch = getattr(self.core_lifecycle, "subagent_orchestrator", None)
82
+ if orch is not None:
83
+ await orch.reload_from_config(data)
84
+
85
+ return jsonify(Response().ok(message="保存成功").__dict__)
86
+ except Exception as e:
87
+ logger.error(traceback.format_exc())
88
+ return jsonify(Response().error(f"保存 subagent 配置失败: {e!s}").__dict__)
89
+
90
+ async def get_available_tools(self):
91
+ """Return all registered tools (name/description/parameters/active/origin).
92
+
93
+ UI can use this to build a multi-select list for subagent tool assignment.
94
+ """
95
+ try:
96
+ tool_mgr = self.core_lifecycle.provider_manager.llm_tools
97
+ tools_dict = []
98
+ for tool in tool_mgr.func_list:
99
+ # Prevent recursive routing: subagents should not be able to select
100
+ # the handoff (transfer_to_*) tools as their own mounted tools.
101
+ if isinstance(tool, HandoffTool):
102
+ continue
103
+ if tool.handler_module_path == "core.subagent_orchestrator":
104
+ continue
105
+ tools_dict.append(
106
+ {
107
+ "name": tool.name,
108
+ "description": tool.description,
109
+ "parameters": tool.parameters,
110
+ "active": tool.active,
111
+ "handler_module_path": tool.handler_module_path,
112
+ }
113
+ )
114
+ return jsonify(Response().ok(data=tools_dict).__dict__)
115
+ except Exception as e:
116
+ logger.error(traceback.format_exc())
117
+ return jsonify(Response().error(f"获取可用工具失败: {e!s}").__dict__)
@@ -130,19 +130,25 @@ class ToolsRoute(Route):
130
130
  server_data = await request.json
131
131
 
132
132
  name = server_data.get("name", "")
133
+ old_name = server_data.get("oldName") or name
133
134
 
134
135
  if not name:
135
136
  return Response().error("服务器名称不能为空").__dict__
136
137
 
137
138
  config = self.tool_mgr.load_mcp_config()
138
139
 
139
- if name not in config["mcpServers"]:
140
- return Response().error(f"服务器 {name} 不存在").__dict__
140
+ if old_name not in config["mcpServers"]:
141
+ return Response().error(f"服务器 {old_name} 不存在").__dict__
142
+
143
+ is_rename = name != old_name
144
+
145
+ if name in config["mcpServers"] and is_rename:
146
+ return Response().error(f"服务器 {name} 已存在").__dict__
141
147
 
142
148
  # 获取活动状态
143
149
  active = server_data.get(
144
150
  "active",
145
- config["mcpServers"][name].get("active", True),
151
+ config["mcpServers"][old_name].get("active", True),
146
152
  )
147
153
 
148
154
  # 创建新的配置对象
@@ -153,7 +159,13 @@ class ToolsRoute(Route):
153
159
 
154
160
  # 复制所有配置字段
155
161
  for key, value in server_data.items():
156
- if key not in ["name", "active", "tools", "errlogs"]: # 排除特殊字段
162
+ if key not in [
163
+ "name",
164
+ "active",
165
+ "tools",
166
+ "errlogs",
167
+ "oldName",
168
+ ]: # 排除特殊字段
157
169
  if key == "mcpServers":
158
170
  key_0 = list(server_data["mcpServers"].keys())[
159
171
  0
@@ -165,29 +177,42 @@ class ToolsRoute(Route):
165
177
 
166
178
  # 如果只更新活动状态,保留原始配置
167
179
  if only_update_active:
168
- for key, value in config["mcpServers"][name].items():
180
+ for key, value in config["mcpServers"][old_name].items():
169
181
  if key != "active": # 除了active之外的所有字段都保留
170
182
  server_config[key] = value
171
183
 
172
- config["mcpServers"][name] = server_config
184
+ # config["mcpServers"][name] = server_config
185
+ if is_rename:
186
+ config["mcpServers"].pop(old_name)
187
+ config["mcpServers"][name] = server_config
188
+ else:
189
+ config["mcpServers"][name] = server_config
173
190
 
174
191
  if self.tool_mgr.save_mcp_config(config):
175
192
  # 处理MCP客户端状态变化
176
193
  if active:
177
- if name in self.tool_mgr.mcp_client_dict or not only_update_active:
194
+ if (
195
+ old_name in self.tool_mgr.mcp_client_dict
196
+ or not only_update_active
197
+ or is_rename
198
+ ):
178
199
  try:
179
- await self.tool_mgr.disable_mcp_server(name, timeout=10)
200
+ await self.tool_mgr.disable_mcp_server(old_name, timeout=10)
180
201
  except TimeoutError as e:
181
202
  return (
182
203
  Response()
183
- .error(f"启用前停用 MCP 服务器时 {name} 超时: {e!s}")
204
+ .error(
205
+ f"启用前停用 MCP 服务器时 {old_name} 超时: {e!s}"
206
+ )
184
207
  .__dict__
185
208
  )
186
209
  except Exception as e:
187
210
  logger.error(traceback.format_exc())
188
211
  return (
189
212
  Response()
190
- .error(f"启用前停用 MCP 服务器时 {name} 失败: {e!s}")
213
+ .error(
214
+ f"启用前停用 MCP 服务器时 {old_name} 失败: {e!s}"
215
+ )
191
216
  .__dict__
192
217
  )
193
218
  try:
@@ -208,18 +233,20 @@ class ToolsRoute(Route):
208
233
  .__dict__
209
234
  )
210
235
  # 如果要停用服务器
211
- elif name in self.tool_mgr.mcp_client_dict:
236
+ elif old_name in self.tool_mgr.mcp_client_dict:
212
237
  try:
213
- await self.tool_mgr.disable_mcp_server(name, timeout=10)
238
+ await self.tool_mgr.disable_mcp_server(old_name, timeout=10)
214
239
  except TimeoutError:
215
240
  return (
216
- Response().error(f"停用 MCP 服务器 {name} 超时。").__dict__
241
+ Response()
242
+ .error(f"停用 MCP 服务器 {old_name} 超时。")
243
+ .__dict__
217
244
  )
218
245
  except Exception as e:
219
246
  logger.error(traceback.format_exc())
220
247
  return (
221
248
  Response()
222
- .error(f"停用 MCP 服务器 {name} 失败: {e!s}")
249
+ .error(f"停用 MCP 服务器 {old_name} 失败: {e!s}")
223
250
  .__dict__
224
251
  )
225
252
 
@@ -26,6 +26,7 @@ from .routes.live_chat import LiveChatRoute
26
26
  from .routes.platform import PlatformRoute
27
27
  from .routes.route import Response, RouteContext
28
28
  from .routes.session_management import SessionManagementRoute
29
+ from .routes.subagent import SubAgentRoute
29
30
  from .routes.t2i import T2iRoute
30
31
 
31
32
  APP: Quart
@@ -79,6 +80,7 @@ class AstrBotDashboard:
79
80
  self.chat_route = ChatRoute(self.context, db, core_lifecycle)
80
81
  self.chatui_project_route = ChatUIProjectRoute(self.context, db)
81
82
  self.tools_root = ToolsRoute(self.context, core_lifecycle)
83
+ self.subagent_route = SubAgentRoute(self.context, core_lifecycle)
82
84
  self.skills_route = SkillsRoute(self.context, core_lifecycle)
83
85
  self.conversation_route = ConversationRoute(self.context, db, core_lifecycle)
84
86
  self.file_route = FileRoute(self.context)
@@ -88,6 +90,7 @@ class AstrBotDashboard:
88
90
  core_lifecycle,
89
91
  )
90
92
  self.persona_route = PersonaRoute(self.context, db, core_lifecycle)
93
+ self.cron_route = CronRoute(self.context, core_lifecycle)
91
94
  self.t2i_route = T2iRoute(self.context, core_lifecycle)
92
95
  self.kb_route = KnowledgeBaseRoute(self.context, core_lifecycle)
93
96
  self.platform_route = PlatformRoute(self.context, core_lifecycle)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: AstrBot
3
- Version: 4.13.2
3
+ Version: 4.14.1
4
4
  Summary: Easy-to-use multi-platform LLM chatbot and development framework
5
5
  License-File: LICENSE
6
6
  Keywords: Astrbot,Astrbot Module,Astrbot Plugin
@@ -96,7 +96,7 @@ Description-Content-Type: text/markdown
96
96
  <a href="https://github.com/AstrBotDevs/AstrBot/issues">问题提交</a>
97
97
  </div>
98
98
 
99
- AstrBot 是一个开源的一站式 Agent 聊天机器人平台,可接入主流即时通讯软件,为个人、开发者和团队打造可靠、可扩展的对话式智能基础设施。无论是个人 AI 伙伴、智能客服、自动化助手,还是企业知识库,AstrBot 都能在你的即时通讯软件平台的工作流中快速构建生产可用的 AI 应用。
99
+ AstrBot 是一个开源的一站式 Agentic 个人和群聊助手,可在 QQ、Telegram、企业微信、飞书、钉钉、Slack、等数十款主流即时通讯软件上部署,此外还内置类似 OpenWebUI 的轻量化 ChatUI,为个人、开发者和团队打造可靠、可扩展的对话式智能基础设施。无论是个人 AI 伙伴、智能客服、自动化助手,还是企业知识库,AstrBot 都能在你的即时通讯软件平台的工作流中快速构建 AI 应用。
100
100
 
101
101
  ![521771166-00782c4c-4437-4d97-aabc-605e3738da5c (1)](https://github.com/user-attachments/assets/61e7b505-f7db-41aa-a75f-4ef8f079b8ba)
102
102
 
@@ -112,6 +112,25 @@ AstrBot 是一个开源的一站式 Agent 聊天机器人平台,可接入主
112
112
  7. 🌈 Web ChatUI 支持,ChatUI 内置代理沙盒、网页搜索等。
113
113
  8. 🌐 国际化(i18n)支持。
114
114
 
115
+ <br>
116
+
117
+ <table align="center">
118
+ <tr align="center">
119
+ <th>💙 角色扮演 & 情感陪伴</th>
120
+ <th>✨ 主动式 Agent</th>
121
+ <th>🚀 通用 Agentic 能力</th>
122
+ <th>🧩 900+ 社区插件</th>
123
+ </tr>
124
+ <tr>
125
+ <td align="center"><p align="center"><img width="984" height="1746" alt="99b587c5d35eea09d84f33e6cf6cfd4f" src="https://github.com/user-attachments/assets/89196061-3290-458d-b51f-afa178049f84" /></p></td>
126
+ <td align="center"><p align="center"><img width="976" height="1612" alt="c449acd838c41d0915cc08a3824025b1" src="https://github.com/user-attachments/assets/f75368b4-e022-41dc-a9e0-131c3e73e32e" /></p></td>
127
+ <td align="center"><p align="center"><img width="974" height="1732" alt="image" src="https://github.com/user-attachments/assets/e22a3968-87d7-4708-a7cd-e7f198c7c32e" /></p></td>
128
+ <td align="center"><p align="center"><img width="976" height="1734" alt="image" src="https://github.com/user-attachments/assets/0952b395-6b4a-432a-8a50-c294b7f89750" /></p></td>
129
+ </tr>
130
+ </table>
131
+
132
+ 陪伴与能力**从来不应该是**对立面。我们希望创造的是一个既能理解情绪、给予陪伴,也能可靠完成工作的机器人——致敬[ATRI](https://zh.wikipedia.org/zh-cn/ATRI_-My_Dear_Moments-)。
133
+
115
134
  ## 快速开始
116
135
 
117
136
  #### Docker 部署(推荐 🥳)