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.
- astrbot/builtin_stars/astrbot/main.py +0 -6
- astrbot/builtin_stars/session_controller/main.py +1 -2
- astrbot/cli/__init__.py +1 -1
- astrbot/core/agent/agent.py +2 -1
- astrbot/core/agent/handoff.py +14 -1
- astrbot/core/agent/runners/tool_loop_agent_runner.py +14 -1
- astrbot/core/agent/tool.py +5 -0
- astrbot/core/astr_agent_run_util.py +21 -3
- astrbot/core/astr_agent_tool_exec.py +178 -3
- astrbot/core/astr_main_agent.py +980 -0
- astrbot/core/astr_main_agent_resources.py +453 -0
- astrbot/core/computer/computer_client.py +10 -1
- astrbot/core/computer/tools/fs.py +22 -14
- astrbot/core/config/default.py +84 -58
- astrbot/core/core_lifecycle.py +43 -1
- astrbot/core/cron/__init__.py +3 -0
- astrbot/core/cron/events.py +67 -0
- astrbot/core/cron/manager.py +376 -0
- astrbot/core/db/__init__.py +60 -0
- astrbot/core/db/po.py +31 -0
- astrbot/core/db/sqlite.py +120 -0
- astrbot/core/event_bus.py +0 -1
- astrbot/core/message/message_event_result.py +21 -3
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +111 -580
- astrbot/core/pipeline/scheduler.py +0 -2
- astrbot/core/platform/astr_message_event.py +5 -5
- astrbot/core/platform/platform.py +9 -0
- astrbot/core/platform/platform_metadata.py +2 -0
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +1 -0
- astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +1 -0
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +1 -0
- astrbot/core/platform/sources/webchat/webchat_adapter.py +1 -0
- astrbot/core/platform/sources/wecom/wecom_adapter.py +1 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +1 -0
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +1 -0
- astrbot/core/provider/entities.py +1 -1
- astrbot/core/skills/skill_manager.py +9 -8
- astrbot/core/star/context.py +8 -0
- astrbot/core/star/filter/custom_filter.py +3 -3
- astrbot/core/star/register/star_handler.py +1 -1
- astrbot/core/subagent_orchestrator.py +96 -0
- astrbot/core/tools/cron_tools.py +174 -0
- astrbot/core/utils/history_saver.py +31 -0
- astrbot/core/utils/trace.py +4 -0
- astrbot/dashboard/routes/__init__.py +4 -0
- astrbot/dashboard/routes/cron.py +174 -0
- astrbot/dashboard/routes/log.py +36 -0
- astrbot/dashboard/routes/plugin.py +11 -0
- astrbot/dashboard/routes/skills.py +12 -37
- astrbot/dashboard/routes/subagent.py +117 -0
- astrbot/dashboard/routes/tools.py +41 -14
- astrbot/dashboard/server.py +3 -0
- {astrbot-4.13.2.dist-info → astrbot-4.14.1.dist-info}/METADATA +21 -2
- {astrbot-4.13.2.dist-info → astrbot-4.14.1.dist-info}/RECORD +57 -51
- astrbot/builtin_stars/astrbot/process_llm_request.py +0 -308
- astrbot/builtin_stars/reminder/main.py +0 -266
- astrbot/builtin_stars/reminder/metadata.yaml +0 -4
- astrbot/core/pipeline/process_stage/utils.py +0 -219
- {astrbot-4.13.2.dist-info → astrbot-4.14.1.dist-info}/WHEEL +0 -0
- {astrbot-4.13.2.dist-info → astrbot-4.14.1.dist-info}/entry_points.txt +0 -0
- {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__)
|
astrbot/dashboard/routes/log.py
CHANGED
|
@@ -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
|
-
|
|
29
|
-
"
|
|
27
|
+
provider_settings = self.core_lifecycle.astrbot_config.get(
|
|
28
|
+
"provider_settings", {}
|
|
30
29
|
)
|
|
31
|
-
runtime =
|
|
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
|
|
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
|
|
140
|
-
return Response().error(f"服务器 {
|
|
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"][
|
|
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 [
|
|
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"][
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
236
|
+
elif old_name in self.tool_mgr.mcp_client_dict:
|
|
212
237
|
try:
|
|
213
|
-
await self.tool_mgr.disable_mcp_server(
|
|
238
|
+
await self.tool_mgr.disable_mcp_server(old_name, timeout=10)
|
|
214
239
|
except TimeoutError:
|
|
215
240
|
return (
|
|
216
|
-
Response()
|
|
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 服务器 {
|
|
249
|
+
.error(f"停用 MCP 服务器 {old_name} 失败: {e!s}")
|
|
223
250
|
.__dict__
|
|
224
251
|
)
|
|
225
252
|
|
astrbot/dashboard/server.py
CHANGED
|
@@ -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.
|
|
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 是一个开源的一站式
|
|
99
|
+
AstrBot 是一个开源的一站式 Agentic 个人和群聊助手,可在 QQ、Telegram、企业微信、飞书、钉钉、Slack、等数十款主流即时通讯软件上部署,此外还内置类似 OpenWebUI 的轻量化 ChatUI,为个人、开发者和团队打造可靠、可扩展的对话式智能基础设施。无论是个人 AI 伙伴、智能客服、自动化助手,还是企业知识库,AstrBot 都能在你的即时通讯软件平台的工作流中快速构建 AI 应用。
|
|
100
100
|
|
|
101
101
|

|
|
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 部署(推荐 🥳)
|