AstrBot 4.12.4__py3-none-any.whl → 4.13.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.
- astrbot/builtin_stars/astrbot/process_llm_request.py +42 -1
- astrbot/cli/__init__.py +1 -1
- astrbot/core/agent/runners/tool_loop_agent_runner.py +91 -1
- astrbot/core/agent/tool.py +61 -20
- astrbot/core/astr_agent_tool_exec.py +2 -2
- astrbot/core/{sandbox → computer}/booters/base.py +4 -4
- astrbot/core/{sandbox → computer}/booters/boxlite.py +2 -2
- astrbot/core/computer/booters/local.py +234 -0
- astrbot/core/{sandbox → computer}/booters/shipyard.py +2 -2
- astrbot/core/computer/computer_client.py +102 -0
- astrbot/core/{sandbox → computer}/tools/__init__.py +2 -1
- astrbot/core/{sandbox → computer}/tools/fs.py +1 -1
- astrbot/core/computer/tools/python.py +94 -0
- astrbot/core/{sandbox → computer}/tools/shell.py +13 -5
- astrbot/core/config/default.py +61 -9
- astrbot/core/db/__init__.py +3 -0
- astrbot/core/db/po.py +4 -0
- astrbot/core/db/sqlite.py +19 -1
- astrbot/core/message/components.py +2 -2
- astrbot/core/persona_mgr.py +8 -0
- astrbot/core/pipeline/context_utils.py +2 -2
- astrbot/core/pipeline/preprocess_stage/stage.py +1 -1
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +16 -2
- astrbot/core/pipeline/process_stage/utils.py +19 -4
- astrbot/core/pipeline/scheduler.py +1 -1
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +3 -3
- astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +5 -7
- astrbot/core/provider/manager.py +31 -0
- astrbot/core/provider/sources/gemini_source.py +12 -9
- astrbot/core/skills/__init__.py +3 -0
- astrbot/core/skills/skill_manager.py +237 -0
- astrbot/core/star/command_management.py +1 -1
- astrbot/core/star/config.py +1 -1
- astrbot/core/star/filter/command.py +1 -1
- astrbot/core/star/filter/custom_filter.py +2 -2
- astrbot/core/star/register/star_handler.py +1 -1
- astrbot/core/utils/astrbot_path.py +6 -0
- astrbot/dashboard/routes/__init__.py +2 -0
- astrbot/dashboard/routes/config.py +236 -2
- astrbot/dashboard/routes/persona.py +7 -0
- astrbot/dashboard/routes/skills.py +148 -0
- astrbot/dashboard/routes/util.py +102 -0
- astrbot/dashboard/server.py +19 -5
- {astrbot-4.12.4.dist-info → astrbot-4.13.0.dist-info}/METADATA +1 -1
- {astrbot-4.12.4.dist-info → astrbot-4.13.0.dist-info}/RECORD +52 -47
- astrbot/core/sandbox/sandbox_client.py +0 -52
- astrbot/core/sandbox/tools/python.py +0 -74
- /astrbot/core/{sandbox → computer}/olayer/__init__.py +0 -0
- /astrbot/core/{sandbox → computer}/olayer/filesystem.py +0 -0
- /astrbot/core/{sandbox → computer}/olayer/python.py +0 -0
- /astrbot/core/{sandbox → computer}/olayer/shell.py +0 -0
- {astrbot-4.12.4.dist-info → astrbot-4.13.0.dist-info}/WHEEL +0 -0
- {astrbot-4.12.4.dist-info → astrbot-4.13.0.dist-info}/entry_points.txt +0 -0
- {astrbot-4.12.4.dist-info → astrbot-4.13.0.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 ..
|
|
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. 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 ..
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
if context.context.event.role != "admin":
|
|
50
|
+
return "error: Permission denied. Shell execution is only allowed for admin users. 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)
|
astrbot/core/config/default.py
CHANGED
|
@@ -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.
|
|
8
|
+
VERSION = "4.13.0"
|
|
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.
|
|
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
|
}
|
astrbot/core/db/__init__.py
CHANGED
|
@@ -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
|
...
|
astrbot/core/db/po.py
CHANGED
|
@@ -125,6 +125,8 @@ class Persona(SQLModel, table=True):
|
|
|
125
125
|
"""a list of strings, each representing a dialog to start with"""
|
|
126
126
|
tools: list | None = Field(default=None, sa_type=JSON)
|
|
127
127
|
"""None means use ALL tools for default, empty list means no tools, otherwise a list of tool names."""
|
|
128
|
+
skills: list | None = Field(default=None, sa_type=JSON)
|
|
129
|
+
"""None means use ALL skills for default, empty list means no skills, otherwise a list of skill names."""
|
|
128
130
|
folder_id: str | None = Field(default=None, max_length=36)
|
|
129
131
|
"""所属文件夹ID,NULL 表示在根目录"""
|
|
130
132
|
sort_order: int = Field(default=0)
|
|
@@ -442,6 +444,8 @@ class Personality(TypedDict):
|
|
|
442
444
|
"""情感模拟对话预设。在 v4.0.0 版本及之后,已被废弃。"""
|
|
443
445
|
tools: list[str] | None
|
|
444
446
|
"""工具列表。None 表示使用所有工具,空列表表示不使用任何工具"""
|
|
447
|
+
skills: list[str] | None
|
|
448
|
+
"""Skills 列表。None 表示使用所有 Skills,空列表表示不使用任何 Skills"""
|
|
445
449
|
|
|
446
450
|
# cache
|
|
447
451
|
_begin_dialogs_processed: list[dict]
|
astrbot/core/db/sqlite.py
CHANGED
|
@@ -52,8 +52,9 @@ class SQLiteDatabase(BaseDatabase):
|
|
|
52
52
|
await conn.execute(text("PRAGMA temp_store=MEMORY"))
|
|
53
53
|
await conn.execute(text("PRAGMA mmap_size=134217728"))
|
|
54
54
|
await conn.execute(text("PRAGMA optimize"))
|
|
55
|
-
# 确保 personas 表有 folder_id
|
|
55
|
+
# 确保 personas 表有 folder_id、sort_order、skills 列(前向兼容)
|
|
56
56
|
await self._ensure_persona_folder_columns(conn)
|
|
57
|
+
await self._ensure_persona_skills_column(conn)
|
|
57
58
|
await conn.commit()
|
|
58
59
|
|
|
59
60
|
async def _ensure_persona_folder_columns(self, conn) -> None:
|
|
@@ -76,6 +77,18 @@ class SQLiteDatabase(BaseDatabase):
|
|
|
76
77
|
text("ALTER TABLE personas ADD COLUMN sort_order INTEGER DEFAULT 0")
|
|
77
78
|
)
|
|
78
79
|
|
|
80
|
+
async def _ensure_persona_skills_column(self, conn) -> None:
|
|
81
|
+
"""确保 personas 表有 skills 列。
|
|
82
|
+
|
|
83
|
+
这是为了支持旧版数据库的平滑升级。新版数据库通过 SQLModel
|
|
84
|
+
的 metadata.create_all 自动创建这些列。
|
|
85
|
+
"""
|
|
86
|
+
result = await conn.execute(text("PRAGMA table_info(personas)"))
|
|
87
|
+
columns = {row[1] for row in result.fetchall()}
|
|
88
|
+
|
|
89
|
+
if "skills" not in columns:
|
|
90
|
+
await conn.execute(text("ALTER TABLE personas ADD COLUMN skills JSON"))
|
|
91
|
+
|
|
79
92
|
# ====
|
|
80
93
|
# Platform Statistics
|
|
81
94
|
# ====
|
|
@@ -564,6 +577,7 @@ class SQLiteDatabase(BaseDatabase):
|
|
|
564
577
|
system_prompt,
|
|
565
578
|
begin_dialogs=None,
|
|
566
579
|
tools=None,
|
|
580
|
+
skills=None,
|
|
567
581
|
folder_id=None,
|
|
568
582
|
sort_order=0,
|
|
569
583
|
):
|
|
@@ -576,6 +590,7 @@ class SQLiteDatabase(BaseDatabase):
|
|
|
576
590
|
system_prompt=system_prompt,
|
|
577
591
|
begin_dialogs=begin_dialogs or [],
|
|
578
592
|
tools=tools,
|
|
593
|
+
skills=skills,
|
|
579
594
|
folder_id=folder_id,
|
|
580
595
|
sort_order=sort_order,
|
|
581
596
|
)
|
|
@@ -606,6 +621,7 @@ class SQLiteDatabase(BaseDatabase):
|
|
|
606
621
|
system_prompt=None,
|
|
607
622
|
begin_dialogs=None,
|
|
608
623
|
tools=NOT_GIVEN,
|
|
624
|
+
skills=NOT_GIVEN,
|
|
609
625
|
):
|
|
610
626
|
"""Update a persona's system prompt or begin dialogs."""
|
|
611
627
|
async with self.get_db() as session:
|
|
@@ -619,6 +635,8 @@ class SQLiteDatabase(BaseDatabase):
|
|
|
619
635
|
values["begin_dialogs"] = begin_dialogs
|
|
620
636
|
if tools is not NOT_GIVEN:
|
|
621
637
|
values["tools"] = tools
|
|
638
|
+
if skills is not NOT_GIVEN:
|
|
639
|
+
values["skills"] = skills
|
|
622
640
|
if not values:
|
|
623
641
|
return None
|
|
624
642
|
query = query.values(**values)
|
|
@@ -567,7 +567,7 @@ class Node(BaseMessageComponent):
|
|
|
567
567
|
async def to_dict(self):
|
|
568
568
|
data_content = []
|
|
569
569
|
for comp in self.content:
|
|
570
|
-
if isinstance(comp,
|
|
570
|
+
if isinstance(comp, Image | Record):
|
|
571
571
|
# For Image and Record segments, we convert them to base64
|
|
572
572
|
bs64 = await comp.convert_to_base64()
|
|
573
573
|
data_content.append(
|
|
@@ -584,7 +584,7 @@ class Node(BaseMessageComponent):
|
|
|
584
584
|
# For File segments, we need to handle the file differently
|
|
585
585
|
d = await comp.to_dict()
|
|
586
586
|
data_content.append(d)
|
|
587
|
-
elif isinstance(comp,
|
|
587
|
+
elif isinstance(comp, Node | Nodes):
|
|
588
588
|
# For Node segments, we recursively convert them to dict
|
|
589
589
|
d = await comp.to_dict()
|
|
590
590
|
data_content.append(d)
|
astrbot/core/persona_mgr.py
CHANGED
|
@@ -10,6 +10,7 @@ DEFAULT_PERSONALITY = Personality(
|
|
|
10
10
|
begin_dialogs=[],
|
|
11
11
|
mood_imitation_dialogs=[],
|
|
12
12
|
tools=None,
|
|
13
|
+
skills=None,
|
|
13
14
|
_begin_dialogs_processed=[],
|
|
14
15
|
_mood_imitation_dialogs_processed="",
|
|
15
16
|
)
|
|
@@ -71,6 +72,7 @@ class PersonaManager:
|
|
|
71
72
|
system_prompt: str | None = None,
|
|
72
73
|
begin_dialogs: list[str] | None = None,
|
|
73
74
|
tools: list[str] | None = None,
|
|
75
|
+
skills: list[str] | None = None,
|
|
74
76
|
):
|
|
75
77
|
"""更新指定 persona 的信息。tools 参数为 None 时表示使用所有工具,空列表表示不使用任何工具"""
|
|
76
78
|
existing_persona = await self.db.get_persona_by_id(persona_id)
|
|
@@ -81,6 +83,7 @@ class PersonaManager:
|
|
|
81
83
|
system_prompt,
|
|
82
84
|
begin_dialogs,
|
|
83
85
|
tools=tools,
|
|
86
|
+
skills=skills,
|
|
84
87
|
)
|
|
85
88
|
if persona:
|
|
86
89
|
for i, p in enumerate(self.personas):
|
|
@@ -239,6 +242,7 @@ class PersonaManager:
|
|
|
239
242
|
system_prompt: str,
|
|
240
243
|
begin_dialogs: list[str] | None = None,
|
|
241
244
|
tools: list[str] | None = None,
|
|
245
|
+
skills: list[str] | None = None,
|
|
242
246
|
folder_id: str | None = None,
|
|
243
247
|
sort_order: int = 0,
|
|
244
248
|
) -> Persona:
|
|
@@ -249,6 +253,7 @@ class PersonaManager:
|
|
|
249
253
|
system_prompt: 系统提示词
|
|
250
254
|
begin_dialogs: 预设对话列表
|
|
251
255
|
tools: 工具列表,None 表示使用所有工具,空列表表示不使用任何工具
|
|
256
|
+
skills: Skills 列表,None 表示使用所有 Skills,空列表表示不使用任何 Skills
|
|
252
257
|
folder_id: 所属文件夹 ID,None 表示根目录
|
|
253
258
|
sort_order: 排序顺序
|
|
254
259
|
"""
|
|
@@ -259,6 +264,7 @@ class PersonaManager:
|
|
|
259
264
|
system_prompt,
|
|
260
265
|
begin_dialogs,
|
|
261
266
|
tools=tools,
|
|
267
|
+
skills=skills,
|
|
262
268
|
folder_id=folder_id,
|
|
263
269
|
sort_order=sort_order,
|
|
264
270
|
)
|
|
@@ -284,6 +290,7 @@ class PersonaManager:
|
|
|
284
290
|
"begin_dialogs": persona.begin_dialogs or [],
|
|
285
291
|
"mood_imitation_dialogs": [], # deprecated
|
|
286
292
|
"tools": persona.tools,
|
|
293
|
+
"skills": persona.skills,
|
|
287
294
|
}
|
|
288
295
|
for persona in self.personas
|
|
289
296
|
]
|
|
@@ -339,6 +346,7 @@ class PersonaManager:
|
|
|
339
346
|
system_prompt=selected_default_persona["prompt"],
|
|
340
347
|
begin_dialogs=selected_default_persona["begin_dialogs"],
|
|
341
348
|
tools=selected_default_persona["tools"] or None,
|
|
349
|
+
skills=selected_default_persona["skills"] or None,
|
|
342
350
|
)
|
|
343
351
|
|
|
344
352
|
return v3_persona_config, personas_v3, selected_default_persona
|
|
@@ -48,7 +48,7 @@ async def call_handler(
|
|
|
48
48
|
# 这里逐步执行异步生成器, 对于每个 yield 返回的 ret, 执行下面的代码
|
|
49
49
|
# 返回值只能是 MessageEventResult 或者 None(无返回值)
|
|
50
50
|
_has_yielded = True
|
|
51
|
-
if isinstance(ret,
|
|
51
|
+
if isinstance(ret, MessageEventResult | CommandResult):
|
|
52
52
|
# 如果返回值是 MessageEventResult, 设置结果并继续
|
|
53
53
|
event.set_result(ret)
|
|
54
54
|
yield
|
|
@@ -65,7 +65,7 @@ async def call_handler(
|
|
|
65
65
|
elif inspect.iscoroutine(ready_to_call):
|
|
66
66
|
# 如果只是一个协程, 直接执行
|
|
67
67
|
ret = await ready_to_call
|
|
68
|
-
if isinstance(ret,
|
|
68
|
+
if isinstance(ret, MessageEventResult | CommandResult):
|
|
69
69
|
event.set_result(ret)
|
|
70
70
|
yield
|
|
71
71
|
else:
|
|
@@ -52,7 +52,7 @@ class PreProcessStage(Stage):
|
|
|
52
52
|
message_chain = event.get_messages()
|
|
53
53
|
|
|
54
54
|
for idx, component in enumerate(message_chain):
|
|
55
|
-
if isinstance(component,
|
|
55
|
+
if isinstance(component, Record | Image) and component.url:
|
|
56
56
|
for mapping in mappings:
|
|
57
57
|
from_, to_ = mapping.split(":")
|
|
58
58
|
from_ = from_.removesuffix("/")
|