AstrBot 4.12.4__py3-none-any.whl → 4.13.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 (54) hide show
  1. astrbot/builtin_stars/astrbot/process_llm_request.py +42 -1
  2. astrbot/cli/__init__.py +1 -1
  3. astrbot/core/agent/runners/tool_loop_agent_runner.py +91 -1
  4. astrbot/core/agent/tool.py +61 -20
  5. astrbot/core/astr_agent_tool_exec.py +2 -2
  6. astrbot/core/{sandbox → computer}/booters/base.py +4 -4
  7. astrbot/core/{sandbox → computer}/booters/boxlite.py +2 -2
  8. astrbot/core/computer/booters/local.py +234 -0
  9. astrbot/core/{sandbox → computer}/booters/shipyard.py +2 -2
  10. astrbot/core/computer/computer_client.py +102 -0
  11. astrbot/core/{sandbox → computer}/tools/__init__.py +2 -1
  12. astrbot/core/{sandbox → computer}/tools/fs.py +1 -1
  13. astrbot/core/computer/tools/python.py +94 -0
  14. astrbot/core/{sandbox → computer}/tools/shell.py +13 -5
  15. astrbot/core/config/default.py +61 -9
  16. astrbot/core/db/__init__.py +3 -0
  17. astrbot/core/db/po.py +23 -61
  18. astrbot/core/db/sqlite.py +19 -1
  19. astrbot/core/message/components.py +2 -2
  20. astrbot/core/persona_mgr.py +8 -0
  21. astrbot/core/pipeline/context_utils.py +2 -2
  22. astrbot/core/pipeline/preprocess_stage/stage.py +1 -1
  23. astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +21 -6
  24. astrbot/core/pipeline/process_stage/utils.py +19 -4
  25. astrbot/core/pipeline/scheduler.py +1 -1
  26. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +3 -3
  27. astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +5 -7
  28. astrbot/core/provider/manager.py +31 -0
  29. astrbot/core/provider/sources/gemini_source.py +12 -9
  30. astrbot/core/skills/__init__.py +3 -0
  31. astrbot/core/skills/skill_manager.py +238 -0
  32. astrbot/core/star/command_management.py +1 -1
  33. astrbot/core/star/config.py +1 -1
  34. astrbot/core/star/filter/command.py +1 -1
  35. astrbot/core/star/filter/custom_filter.py +2 -2
  36. astrbot/core/star/register/star_handler.py +1 -1
  37. astrbot/core/utils/astrbot_path.py +6 -0
  38. astrbot/dashboard/routes/__init__.py +2 -0
  39. astrbot/dashboard/routes/config.py +236 -2
  40. astrbot/dashboard/routes/persona.py +7 -0
  41. astrbot/dashboard/routes/skills.py +148 -0
  42. astrbot/dashboard/routes/util.py +102 -0
  43. astrbot/dashboard/server.py +19 -5
  44. {astrbot-4.12.4.dist-info → astrbot-4.13.1.dist-info}/METADATA +2 -2
  45. {astrbot-4.12.4.dist-info → astrbot-4.13.1.dist-info}/RECORD +52 -47
  46. astrbot/core/sandbox/sandbox_client.py +0 -52
  47. astrbot/core/sandbox/tools/python.py +0 -74
  48. /astrbot/core/{sandbox → computer}/olayer/__init__.py +0 -0
  49. /astrbot/core/{sandbox → computer}/olayer/filesystem.py +0 -0
  50. /astrbot/core/{sandbox → computer}/olayer/python.py +0 -0
  51. /astrbot/core/{sandbox → computer}/olayer/shell.py +0 -0
  52. {astrbot-4.12.4.dist-info → astrbot-4.13.1.dist-info}/WHEEL +0 -0
  53. {astrbot-4.12.4.dist-info → astrbot-4.13.1.dist-info}/entry_points.txt +0 -0
  54. {astrbot-4.12.4.dist-info → astrbot-4.13.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,102 @@
1
+ import os
2
+ import shutil
3
+ import uuid
4
+ from pathlib import Path
5
+
6
+ from astrbot.api import logger
7
+ from astrbot.core.skills.skill_manager import SANDBOX_SKILLS_ROOT
8
+ from astrbot.core.star.context import Context
9
+ from astrbot.core.utils.astrbot_path import (
10
+ get_astrbot_skills_path,
11
+ get_astrbot_temp_path,
12
+ )
13
+
14
+ from .booters.base import ComputerBooter
15
+ from .booters.local import LocalBooter
16
+
17
+ session_booter: dict[str, ComputerBooter] = {}
18
+ local_booter: ComputerBooter | None = None
19
+
20
+
21
+ async def _sync_skills_to_sandbox(booter: ComputerBooter) -> None:
22
+ skills_root = get_astrbot_skills_path()
23
+ if not os.path.isdir(skills_root):
24
+ return
25
+ if not any(Path(skills_root).iterdir()):
26
+ return
27
+
28
+ temp_dir = get_astrbot_temp_path()
29
+ os.makedirs(temp_dir, exist_ok=True)
30
+ zip_base = os.path.join(temp_dir, "skills_bundle")
31
+ zip_path = f"{zip_base}.zip"
32
+
33
+ try:
34
+ if os.path.exists(zip_path):
35
+ os.remove(zip_path)
36
+ shutil.make_archive(zip_base, "zip", skills_root)
37
+ remote_zip = Path(SANDBOX_SKILLS_ROOT) / "skills.zip"
38
+ await booter.shell.exec(f"mkdir -p {SANDBOX_SKILLS_ROOT}")
39
+ upload_result = await booter.upload_file(zip_path, str(remote_zip))
40
+ if not upload_result.get("success", False):
41
+ raise RuntimeError("Failed to upload skills bundle to sandbox.")
42
+ await booter.shell.exec(
43
+ f"unzip -o {remote_zip} -d {SANDBOX_SKILLS_ROOT} && rm -f {remote_zip}"
44
+ )
45
+ finally:
46
+ if os.path.exists(zip_path):
47
+ try:
48
+ os.remove(zip_path)
49
+ except Exception:
50
+ logger.warning(f"Failed to remove temp skills zip: {zip_path}")
51
+
52
+
53
+ async def get_booter(
54
+ context: Context,
55
+ session_id: str,
56
+ ) -> ComputerBooter:
57
+ config = context.get_config(umo=session_id)
58
+
59
+ sandbox_cfg = config.get("provider_settings", {}).get("sandbox", {})
60
+ booter_type = sandbox_cfg.get("booter", "shipyard")
61
+
62
+ if session_id in session_booter:
63
+ booter = session_booter[session_id]
64
+ if not await booter.available():
65
+ # rebuild
66
+ session_booter.pop(session_id, None)
67
+ if session_id not in session_booter:
68
+ uuid_str = uuid.uuid5(uuid.NAMESPACE_DNS, session_id).hex
69
+ if booter_type == "shipyard":
70
+ from .booters.shipyard import ShipyardBooter
71
+
72
+ ep = sandbox_cfg.get("shipyard_endpoint", "")
73
+ token = sandbox_cfg.get("shipyard_access_token", "")
74
+ ttl = sandbox_cfg.get("shipyard_ttl", 3600)
75
+ max_sessions = sandbox_cfg.get("shipyard_max_sessions", 10)
76
+
77
+ client = ShipyardBooter(
78
+ endpoint_url=ep, access_token=token, ttl=ttl, session_num=max_sessions
79
+ )
80
+ elif booter_type == "boxlite":
81
+ from .booters.boxlite import BoxliteBooter
82
+
83
+ client = BoxliteBooter()
84
+ else:
85
+ raise ValueError(f"Unknown booter type: {booter_type}")
86
+
87
+ try:
88
+ await client.boot(uuid_str)
89
+ await _sync_skills_to_sandbox(client)
90
+ except Exception as e:
91
+ logger.error(f"Error booting sandbox for session {session_id}: {e}")
92
+ raise e
93
+
94
+ session_booter[session_id] = client
95
+ return session_booter[session_id]
96
+
97
+
98
+ def get_local_booter() -> ComputerBooter:
99
+ global local_booter
100
+ if local_booter is None:
101
+ local_booter = LocalBooter()
102
+ return local_booter
@@ -1,10 +1,11 @@
1
1
  from .fs import FileDownloadTool, FileUploadTool
2
- from .python import PythonTool
2
+ from .python import LocalPythonTool, PythonTool
3
3
  from .shell import ExecuteShellTool
4
4
 
5
5
  __all__ = [
6
6
  "FileUploadTool",
7
7
  "PythonTool",
8
+ "LocalPythonTool",
8
9
  "ExecuteShellTool",
9
10
  "FileDownloadTool",
10
11
  ]
@@ -9,7 +9,7 @@ from astrbot.core.astr_agent_context import AstrAgentContext
9
9
  from astrbot.core.message.components import File
10
10
  from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
11
11
 
12
- from ..sandbox_client import get_booter
12
+ from ..computer_client import get_booter
13
13
 
14
14
  # @dataclass
15
15
  # class CreateFileTool(FunctionTool):
@@ -0,0 +1,94 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ import mcp
4
+
5
+ from astrbot.api import FunctionTool
6
+ from astrbot.core.agent.run_context import ContextWrapper
7
+ from astrbot.core.agent.tool import ToolExecResult
8
+ from astrbot.core.astr_agent_context import AstrAgentContext
9
+ from astrbot.core.computer.computer_client import get_booter, get_local_booter
10
+
11
+ param_schema = {
12
+ "type": "object",
13
+ "properties": {
14
+ "code": {
15
+ "type": "string",
16
+ "description": "The Python code to execute.",
17
+ },
18
+ "silent": {
19
+ "type": "boolean",
20
+ "description": "Whether to suppress the output of the code execution.",
21
+ "default": False,
22
+ },
23
+ },
24
+ "required": ["code"],
25
+ }
26
+
27
+
28
+ def handle_result(result: dict) -> ToolExecResult:
29
+ data = result.get("data", {})
30
+ output = data.get("output", {})
31
+ error = data.get("error", "")
32
+ images: list[dict] = output.get("images", [])
33
+ text: str = output.get("text", "")
34
+
35
+ resp = mcp.types.CallToolResult(content=[])
36
+
37
+ if error:
38
+ resp.content.append(mcp.types.TextContent(type="text", text=f"error: {error}"))
39
+
40
+ if images:
41
+ for img in images:
42
+ resp.content.append(
43
+ mcp.types.ImageContent(
44
+ type="image", data=img["image/png"], mimeType="image/png"
45
+ )
46
+ )
47
+ if text:
48
+ resp.content.append(mcp.types.TextContent(type="text", text=text))
49
+
50
+ if not resp.content:
51
+ resp.content.append(mcp.types.TextContent(type="text", text="No output."))
52
+
53
+ return resp
54
+
55
+
56
+ @dataclass
57
+ class PythonTool(FunctionTool):
58
+ name: str = "astrbot_execute_ipython"
59
+ description: str = "Run codes in an IPython shell."
60
+ parameters: dict = field(default_factory=lambda: param_schema)
61
+
62
+ async def call(
63
+ self, context: ContextWrapper[AstrAgentContext], code: str, silent: bool = False
64
+ ) -> ToolExecResult:
65
+ sb = await get_booter(
66
+ context.context.context,
67
+ context.context.event.unified_msg_origin,
68
+ )
69
+ try:
70
+ result = await sb.python.exec(code, silent=silent)
71
+ return handle_result(result)
72
+ except Exception as e:
73
+ return f"Error executing code: {str(e)}"
74
+
75
+
76
+ @dataclass
77
+ class LocalPythonTool(FunctionTool):
78
+ name: str = "astrbot_execute_python"
79
+ description: str = "Execute codes in a Python environment."
80
+
81
+ parameters: dict = field(default_factory=lambda: param_schema)
82
+
83
+ async def call(
84
+ self, context: ContextWrapper[AstrAgentContext], code: str, silent: bool = False
85
+ ) -> ToolExecResult:
86
+ if context.context.event.role != "admin":
87
+ return "error: Permission denied. Local Python execution is only allowed for admin users. Tell user to set admins in AstrBot WebUI."
88
+
89
+ sb = get_local_booter()
90
+ try:
91
+ result = await sb.python.exec(code, silent=silent)
92
+ return handle_result(result)
93
+ except Exception as e:
94
+ return f"Error executing code: {str(e)}"
@@ -6,7 +6,7 @@ from astrbot.core.agent.run_context import ContextWrapper
6
6
  from astrbot.core.agent.tool import ToolExecResult
7
7
  from astrbot.core.astr_agent_context import AstrAgentContext
8
8
 
9
- from ..sandbox_client import get_booter
9
+ from ..computer_client import get_booter, get_local_booter
10
10
 
11
11
 
12
12
  @dataclass
@@ -37,6 +37,8 @@ class ExecuteShellTool(FunctionTool):
37
37
  }
38
38
  )
39
39
 
40
+ is_local: bool = False
41
+
40
42
  async def call(
41
43
  self,
42
44
  context: ContextWrapper[AstrAgentContext],
@@ -44,10 +46,16 @@ class ExecuteShellTool(FunctionTool):
44
46
  background: bool = False,
45
47
  env: dict = {},
46
48
  ) -> ToolExecResult:
47
- sb = await get_booter(
48
- context.context.context,
49
- context.context.event.unified_msg_origin,
50
- )
49
+ if context.context.event.role != "admin":
50
+ return "error: Permission denied. Shell execution is only allowed for admin users. Tell user to Set admins in AstrBot WebUI."
51
+
52
+ if self.is_local:
53
+ sb = get_local_booter()
54
+ else:
55
+ sb = await get_booter(
56
+ context.context.context,
57
+ context.context.event.unified_msg_origin,
58
+ )
51
59
  try:
52
60
  result = await sb.shell.exec(command, background=background, env=env)
53
61
  return json.dumps(result)
@@ -5,7 +5,7 @@ from typing import Any, TypedDict
5
5
 
6
6
  from astrbot.core.utils.astrbot_path import get_astrbot_data_path
7
7
 
8
- VERSION = "4.12.4"
8
+ VERSION = "4.13.1"
9
9
  DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
10
10
 
11
11
  WEBHOOK_SUPPORTED_PLATFORMS = [
@@ -106,6 +106,7 @@ DEFAULT_CONFIG = {
106
106
  "reachability_check": False,
107
107
  "max_agent_step": 30,
108
108
  "tool_call_timeout": 60,
109
+ "tool_schema_mode": "full",
109
110
  "llm_safety_mode": True,
110
111
  "safety_mode_strategy": "system_prompt", # TODO: llm judge
111
112
  "file_extract": {
@@ -121,6 +122,7 @@ DEFAULT_CONFIG = {
121
122
  "shipyard_ttl": 3600,
122
123
  "shipyard_max_sessions": 10,
123
124
  },
125
+ "skills": {"runtime": "sandbox"},
124
126
  },
125
127
  "provider_stt_settings": {
126
128
  "enable": False,
@@ -166,6 +168,7 @@ DEFAULT_CONFIG = {
166
168
  "jwt_secret": "",
167
169
  "host": "0.0.0.0",
168
170
  "port": 6185,
171
+ "disable_access_log": True,
169
172
  },
170
173
  "platform": [],
171
174
  "platform_specific": {
@@ -773,27 +776,21 @@ CONFIG_METADATA_2 = {
773
776
  "interval_method": {
774
777
  "type": "string",
775
778
  "options": ["random", "log"],
776
- "hint": "分段回复的间隔时间计算方法。random 为随机时间,log 为根据消息长度计算,$y=log_<log_base>(x)$,x为字数,y的单位为秒。",
777
779
  },
778
780
  "interval": {
779
781
  "type": "string",
780
- "hint": "`random` 方法用。每一段回复的间隔时间,格式为 `最小时间,最大时间`。如 `0.75,2.5`",
781
782
  },
782
783
  "log_base": {
783
784
  "type": "float",
784
- "hint": "`log` 方法用。对数函数的底数。默认为 2.6",
785
785
  },
786
786
  "words_count_threshold": {
787
787
  "type": "int",
788
- "hint": "分段回复的字数上限。只有字数小于此值的消息才会被分段,超过此值的长消息将直接发送(不分段)。默认为 150",
789
788
  },
790
789
  "regex": {
791
790
  "type": "string",
792
- "hint": "用于分隔一段消息。默认情况下会根据句号、问号等标点符号分隔。re.findall(r'<regex>', text)",
793
791
  },
794
792
  "content_cleanup_rule": {
795
793
  "type": "string",
796
- "hint": "移除分段后的内容中的指定的内容。支持正则表达式。如填写 `[。?!]` 将移除所有的句号、问号、感叹号。re.sub(r'<regex>', '', text)",
797
794
  },
798
795
  },
799
796
  },
@@ -2187,6 +2184,9 @@ CONFIG_METADATA_2 = {
2187
2184
  "tool_call_timeout": {
2188
2185
  "type": "int",
2189
2186
  },
2187
+ "tool_schema_mode": {
2188
+ "type": "string",
2189
+ },
2190
2190
  "file_extract": {
2191
2191
  "type": "object",
2192
2192
  "items": {
@@ -2201,6 +2201,17 @@ CONFIG_METADATA_2 = {
2201
2201
  },
2202
2202
  },
2203
2203
  },
2204
+ "skills": {
2205
+ "type": "object",
2206
+ "items": {
2207
+ "enable": {
2208
+ "type": "bool",
2209
+ },
2210
+ "runtime": {
2211
+ "type": "string",
2212
+ },
2213
+ },
2214
+ },
2204
2215
  },
2205
2216
  },
2206
2217
  "provider_stt_settings": {
@@ -2578,6 +2589,7 @@ CONFIG_METADATA_3 = {
2578
2589
  # },
2579
2590
  "sandbox": {
2580
2591
  "description": "Agent 沙箱环境",
2592
+ "hint": "",
2581
2593
  "type": "object",
2582
2594
  "items": {
2583
2595
  "provider_settings.sandbox.enable": {
@@ -2589,6 +2601,7 @@ CONFIG_METADATA_3 = {
2589
2601
  "description": "沙箱环境驱动器",
2590
2602
  "type": "string",
2591
2603
  "options": ["shipyard"],
2604
+ "labels": ["Shipyard"],
2592
2605
  "condition": {
2593
2606
  "provider_settings.sandbox.enable": True,
2594
2607
  },
@@ -2631,6 +2644,27 @@ CONFIG_METADATA_3 = {
2631
2644
  },
2632
2645
  },
2633
2646
  },
2647
+ "condition": {
2648
+ "provider_settings.agent_runner_type": "local",
2649
+ "provider_settings.enable": True,
2650
+ },
2651
+ },
2652
+ "skills": {
2653
+ "description": "Skills",
2654
+ "type": "object",
2655
+ "items": {
2656
+ "provider_settings.skills.runtime": {
2657
+ "description": "Skill Runtime",
2658
+ "type": "string",
2659
+ "options": ["local", "sandbox"],
2660
+ "labels": ["本地", "沙箱"],
2661
+ "hint": "选择 Skills 运行环境。使用沙箱时需先启用沙箱环境。",
2662
+ },
2663
+ },
2664
+ "condition": {
2665
+ "provider_settings.agent_runner_type": "local",
2666
+ "provider_settings.enable": True,
2667
+ },
2634
2668
  },
2635
2669
  "truncate_and_compress": {
2636
2670
  "description": "上下文管理策略",
@@ -2691,6 +2725,10 @@ CONFIG_METADATA_3 = {
2691
2725
  },
2692
2726
  },
2693
2727
  },
2728
+ "condition": {
2729
+ "provider_settings.agent_runner_type": "local",
2730
+ "provider_settings.enable": True,
2731
+ },
2694
2732
  },
2695
2733
  "others": {
2696
2734
  "description": "其他配置",
@@ -2778,6 +2816,16 @@ CONFIG_METADATA_3 = {
2778
2816
  "provider_settings.agent_runner_type": "local",
2779
2817
  },
2780
2818
  },
2819
+ "provider_settings.tool_schema_mode": {
2820
+ "description": "工具调用模式",
2821
+ "type": "string",
2822
+ "options": ["skills_like", "full"],
2823
+ "labels": ["Skills-like(两阶段)", "Full(完整参数)"],
2824
+ "hint": "skills-like 先下发工具名称与描述,再下发参数;full 一次性下发完整参数。",
2825
+ "condition": {
2826
+ "provider_settings.agent_runner_type": "local",
2827
+ },
2828
+ },
2781
2829
  "provider_settings.wake_prefix": {
2782
2830
  "description": "LLM 聊天额外唤醒前缀 ",
2783
2831
  "type": "string",
@@ -3045,7 +3093,8 @@ CONFIG_METADATA_3 = {
3045
3093
  "type": "bool",
3046
3094
  },
3047
3095
  "platform_settings.segmented_reply.interval_method": {
3048
- "description": "间隔方法",
3096
+ "description": "间隔方法。",
3097
+ "hint": "random 为随机时间,log 为根据消息长度计算,$y=log_<log_base>(x)$,x为字数,y的单位为秒。",
3049
3098
  "type": "string",
3050
3099
  "options": ["random", "log"],
3051
3100
  },
@@ -3060,13 +3109,14 @@ CONFIG_METADATA_3 = {
3060
3109
  "platform_settings.segmented_reply.log_base": {
3061
3110
  "description": "对数底数",
3062
3111
  "type": "float",
3063
- "hint": "对数间隔的底数,默认为 2.0。取值范围为 1.0-10.0。",
3112
+ "hint": "对数间隔的底数,默认为 2.6。取值范围为 1.0-10.0。",
3064
3113
  "condition": {
3065
3114
  "platform_settings.segmented_reply.interval_method": "log",
3066
3115
  },
3067
3116
  },
3068
3117
  "platform_settings.segmented_reply.words_count_threshold": {
3069
3118
  "description": "分段回复字数阈值",
3119
+ "hint": "分段回复的字数上限。只有字数小于此值的消息才会被分段,超过此值的长消息将直接发送(不分段)。默认为 150",
3070
3120
  "type": "int",
3071
3121
  },
3072
3122
  "platform_settings.segmented_reply.split_mode": {
@@ -3077,6 +3127,7 @@ CONFIG_METADATA_3 = {
3077
3127
  },
3078
3128
  "platform_settings.segmented_reply.regex": {
3079
3129
  "description": "分段正则表达式",
3130
+ "hint": "用于分隔一段消息。默认情况下会根据句号、问号等标点符号分隔。如填写 `[。?!]` 将移除所有的句号、问号、感叹号。re.findall(r'<regex>', text)",
3080
3131
  "type": "string",
3081
3132
  "condition": {
3082
3133
  "platform_settings.segmented_reply.split_mode": "regex",
@@ -3246,6 +3297,7 @@ DEFAULT_VALUE_MAP = {
3246
3297
  "string": "",
3247
3298
  "text": "",
3248
3299
  "list": [],
3300
+ "file": [],
3249
3301
  "object": {},
3250
3302
  "template_list": [],
3251
3303
  }
@@ -254,6 +254,7 @@ class BaseDatabase(abc.ABC):
254
254
  system_prompt: str,
255
255
  begin_dialogs: list[str] | None = None,
256
256
  tools: list[str] | None = None,
257
+ skills: list[str] | None = None,
257
258
  folder_id: str | None = None,
258
259
  sort_order: int = 0,
259
260
  ) -> Persona:
@@ -264,6 +265,7 @@ class BaseDatabase(abc.ABC):
264
265
  system_prompt: System prompt for the persona
265
266
  begin_dialogs: Optional list of initial dialog strings
266
267
  tools: Optional list of tool names (None means all tools, [] means no tools)
268
+ skills: Optional list of skill names (None means all skills, [] means no skills)
267
269
  folder_id: Optional folder ID to place the persona in (None means root)
268
270
  sort_order: Sort order within the folder (default 0)
269
271
  """
@@ -286,6 +288,7 @@ class BaseDatabase(abc.ABC):
286
288
  system_prompt: str | None = None,
287
289
  begin_dialogs: list[str] | None = None,
288
290
  tools: list[str] | None = None,
291
+ skills: list[str] | None = None,
289
292
  ) -> Persona | None:
290
293
  """Update a persona's system prompt or begin dialogs."""
291
294
  ...