AstrBot 4.12.3__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.
Files changed (72) hide show
  1. astrbot/builtin_stars/astrbot/process_llm_request.py +42 -1
  2. astrbot/builtin_stars/builtin_commands/commands/__init__.py +0 -2
  3. astrbot/builtin_stars/builtin_commands/commands/persona.py +68 -6
  4. astrbot/builtin_stars/builtin_commands/main.py +0 -26
  5. astrbot/cli/__init__.py +1 -1
  6. astrbot/core/agent/runners/tool_loop_agent_runner.py +91 -1
  7. astrbot/core/agent/tool.py +61 -20
  8. astrbot/core/astr_agent_hooks.py +3 -1
  9. astrbot/core/astr_agent_run_util.py +243 -1
  10. astrbot/core/astr_agent_tool_exec.py +2 -2
  11. astrbot/core/{sandbox → computer}/booters/base.py +4 -4
  12. astrbot/core/{sandbox → computer}/booters/boxlite.py +2 -2
  13. astrbot/core/computer/booters/local.py +234 -0
  14. astrbot/core/{sandbox → computer}/booters/shipyard.py +2 -2
  15. astrbot/core/computer/computer_client.py +102 -0
  16. astrbot/core/{sandbox → computer}/tools/__init__.py +2 -1
  17. astrbot/core/{sandbox → computer}/tools/fs.py +1 -1
  18. astrbot/core/computer/tools/python.py +94 -0
  19. astrbot/core/{sandbox → computer}/tools/shell.py +13 -5
  20. astrbot/core/config/default.py +90 -9
  21. astrbot/core/db/__init__.py +94 -1
  22. astrbot/core/db/po.py +46 -0
  23. astrbot/core/db/sqlite.py +248 -0
  24. astrbot/core/message/components.py +2 -2
  25. astrbot/core/persona_mgr.py +162 -2
  26. astrbot/core/pipeline/context_utils.py +2 -2
  27. astrbot/core/pipeline/preprocess_stage/stage.py +1 -1
  28. astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +73 -6
  29. astrbot/core/pipeline/process_stage/utils.py +31 -4
  30. astrbot/core/pipeline/scheduler.py +1 -1
  31. astrbot/core/pipeline/waking_check/stage.py +0 -1
  32. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +3 -3
  33. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +32 -14
  34. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +61 -2
  35. astrbot/core/platform/sources/dingtalk/dingtalk_event.py +57 -11
  36. astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +5 -7
  37. astrbot/core/platform/sources/webchat/webchat_adapter.py +1 -0
  38. astrbot/core/platform/sources/webchat/webchat_event.py +24 -0
  39. astrbot/core/provider/manager.py +38 -0
  40. astrbot/core/provider/provider.py +54 -0
  41. astrbot/core/provider/sources/gemini_embedding_source.py +1 -1
  42. astrbot/core/provider/sources/gemini_source.py +12 -9
  43. astrbot/core/provider/sources/genie_tts.py +128 -0
  44. astrbot/core/provider/sources/openai_embedding_source.py +1 -1
  45. astrbot/core/skills/__init__.py +3 -0
  46. astrbot/core/skills/skill_manager.py +237 -0
  47. astrbot/core/star/command_management.py +1 -1
  48. astrbot/core/star/config.py +1 -1
  49. astrbot/core/star/context.py +9 -8
  50. astrbot/core/star/filter/command.py +1 -1
  51. astrbot/core/star/filter/custom_filter.py +2 -2
  52. astrbot/core/star/register/star_handler.py +2 -4
  53. astrbot/core/utils/astrbot_path.py +6 -0
  54. astrbot/dashboard/routes/__init__.py +2 -0
  55. astrbot/dashboard/routes/config.py +236 -2
  56. astrbot/dashboard/routes/live_chat.py +423 -0
  57. astrbot/dashboard/routes/persona.py +265 -1
  58. astrbot/dashboard/routes/skills.py +148 -0
  59. astrbot/dashboard/routes/util.py +102 -0
  60. astrbot/dashboard/server.py +21 -5
  61. {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/METADATA +1 -1
  62. {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/RECORD +69 -63
  63. astrbot/builtin_stars/builtin_commands/commands/tool.py +0 -31
  64. astrbot/core/sandbox/sandbox_client.py +0 -52
  65. astrbot/core/sandbox/tools/python.py +0 -74
  66. /astrbot/core/{sandbox → computer}/olayer/__init__.py +0 -0
  67. /astrbot/core/{sandbox → computer}/olayer/filesystem.py +0 -0
  68. /astrbot/core/{sandbox → computer}/olayer/python.py +0 -0
  69. /astrbot/core/{sandbox → computer}/olayer/shell.py +0 -0
  70. {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/WHEEL +0 -0
  71. {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/entry_points.txt +0 -0
  72. {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/licenses/LICENSE +0 -0
@@ -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. 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. 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.3"
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": {
@@ -321,6 +324,7 @@ CONFIG_METADATA_2 = {
321
324
  "enable": False,
322
325
  "client_id": "",
323
326
  "client_secret": "",
327
+ "card_template_id": "",
324
328
  },
325
329
  "Telegram": {
326
330
  "id": "telegram",
@@ -582,6 +586,11 @@ CONFIG_METADATA_2 = {
582
586
  "type": "string",
583
587
  "hint": "可选:填写 Misskey 网盘中目标文件夹的 ID,上传的文件将放置到该文件夹内。留空则使用账号网盘根目录。",
584
588
  },
589
+ "card_template_id": {
590
+ "description": "卡片模板 ID",
591
+ "type": "string",
592
+ "hint": "可选。钉钉互动卡片模板 ID。启用后将使用互动卡片进行流式回复。",
593
+ },
585
594
  "telegram_command_register": {
586
595
  "description": "Telegram 命令注册",
587
596
  "type": "bool",
@@ -767,27 +776,21 @@ CONFIG_METADATA_2 = {
767
776
  "interval_method": {
768
777
  "type": "string",
769
778
  "options": ["random", "log"],
770
- "hint": "分段回复的间隔时间计算方法。random 为随机时间,log 为根据消息长度计算,$y=log_<log_base>(x)$,x为字数,y的单位为秒。",
771
779
  },
772
780
  "interval": {
773
781
  "type": "string",
774
- "hint": "`random` 方法用。每一段回复的间隔时间,格式为 `最小时间,最大时间`。如 `0.75,2.5`",
775
782
  },
776
783
  "log_base": {
777
784
  "type": "float",
778
- "hint": "`log` 方法用。对数函数的底数。默认为 2.6",
779
785
  },
780
786
  "words_count_threshold": {
781
787
  "type": "int",
782
- "hint": "分段回复的字数上限。只有字数小于此值的消息才会被分段,超过此值的长消息将直接发送(不分段)。默认为 150",
783
788
  },
784
789
  "regex": {
785
790
  "type": "string",
786
- "hint": "用于分隔一段消息。默认情况下会根据句号、问号等标点符号分隔。re.findall(r'<regex>', text)",
787
791
  },
788
792
  "content_cleanup_rule": {
789
793
  "type": "string",
790
- "hint": "移除分段后的内容中的指定的内容。支持正则表达式。如填写 `[。?!]` 将移除所有的句号、问号、感叹号。re.sub(r'<regex>', '', text)",
791
794
  },
792
795
  },
793
796
  },
@@ -1179,6 +1182,19 @@ CONFIG_METADATA_2 = {
1179
1182
  "openai-tts-voice": "alloy",
1180
1183
  "timeout": "20",
1181
1184
  },
1185
+ "Genie TTS": {
1186
+ "id": "genie_tts",
1187
+ "provider": "genie_tts",
1188
+ "type": "genie_tts",
1189
+ "provider_type": "text_to_speech",
1190
+ "enable": False,
1191
+ "genie_character_name": "mika",
1192
+ "genie_onnx_model_dir": "CharacterModels/v2ProPlus/mika/tts_models",
1193
+ "genie_language": "Japanese",
1194
+ "genie_refer_audio_path": "",
1195
+ "genie_refer_text": "",
1196
+ "timeout": 20,
1197
+ },
1182
1198
  "Edge TTS": {
1183
1199
  "id": "edge_tts",
1184
1200
  "provider": "microsoft",
@@ -1395,6 +1411,16 @@ CONFIG_METADATA_2 = {
1395
1411
  },
1396
1412
  },
1397
1413
  "items": {
1414
+ "genie_onnx_model_dir": {
1415
+ "description": "ONNX Model Directory",
1416
+ "type": "string",
1417
+ "hint": "The directory path containing the ONNX model files",
1418
+ },
1419
+ "genie_language": {
1420
+ "description": "Language",
1421
+ "type": "string",
1422
+ "options": ["Japanese", "English", "Chinese"],
1423
+ },
1398
1424
  "provider_source_id": {
1399
1425
  "invisible": True,
1400
1426
  "type": "string",
@@ -2158,6 +2184,9 @@ CONFIG_METADATA_2 = {
2158
2184
  "tool_call_timeout": {
2159
2185
  "type": "int",
2160
2186
  },
2187
+ "tool_schema_mode": {
2188
+ "type": "string",
2189
+ },
2161
2190
  "file_extract": {
2162
2191
  "type": "object",
2163
2192
  "items": {
@@ -2172,6 +2201,17 @@ CONFIG_METADATA_2 = {
2172
2201
  },
2173
2202
  },
2174
2203
  },
2204
+ "skills": {
2205
+ "type": "object",
2206
+ "items": {
2207
+ "enable": {
2208
+ "type": "bool",
2209
+ },
2210
+ "runtime": {
2211
+ "type": "string",
2212
+ },
2213
+ },
2214
+ },
2175
2215
  },
2176
2216
  },
2177
2217
  "provider_stt_settings": {
@@ -2549,6 +2589,7 @@ CONFIG_METADATA_3 = {
2549
2589
  # },
2550
2590
  "sandbox": {
2551
2591
  "description": "Agent 沙箱环境",
2592
+ "hint": "",
2552
2593
  "type": "object",
2553
2594
  "items": {
2554
2595
  "provider_settings.sandbox.enable": {
@@ -2560,6 +2601,7 @@ CONFIG_METADATA_3 = {
2560
2601
  "description": "沙箱环境驱动器",
2561
2602
  "type": "string",
2562
2603
  "options": ["shipyard"],
2604
+ "labels": ["Shipyard"],
2563
2605
  "condition": {
2564
2606
  "provider_settings.sandbox.enable": True,
2565
2607
  },
@@ -2602,6 +2644,27 @@ CONFIG_METADATA_3 = {
2602
2644
  },
2603
2645
  },
2604
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
+ },
2605
2668
  },
2606
2669
  "truncate_and_compress": {
2607
2670
  "description": "上下文管理策略",
@@ -2662,6 +2725,10 @@ CONFIG_METADATA_3 = {
2662
2725
  },
2663
2726
  },
2664
2727
  },
2728
+ "condition": {
2729
+ "provider_settings.agent_runner_type": "local",
2730
+ "provider_settings.enable": True,
2731
+ },
2665
2732
  },
2666
2733
  "others": {
2667
2734
  "description": "其他配置",
@@ -2749,6 +2816,16 @@ CONFIG_METADATA_3 = {
2749
2816
  "provider_settings.agent_runner_type": "local",
2750
2817
  },
2751
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
+ },
2752
2829
  "provider_settings.wake_prefix": {
2753
2830
  "description": "LLM 聊天额外唤醒前缀 ",
2754
2831
  "type": "string",
@@ -3016,7 +3093,8 @@ CONFIG_METADATA_3 = {
3016
3093
  "type": "bool",
3017
3094
  },
3018
3095
  "platform_settings.segmented_reply.interval_method": {
3019
- "description": "间隔方法",
3096
+ "description": "间隔方法。",
3097
+ "hint": "random 为随机时间,log 为根据消息长度计算,$y=log_<log_base>(x)$,x为字数,y的单位为秒。",
3020
3098
  "type": "string",
3021
3099
  "options": ["random", "log"],
3022
3100
  },
@@ -3031,13 +3109,14 @@ CONFIG_METADATA_3 = {
3031
3109
  "platform_settings.segmented_reply.log_base": {
3032
3110
  "description": "对数底数",
3033
3111
  "type": "float",
3034
- "hint": "对数间隔的底数,默认为 2.0。取值范围为 1.0-10.0。",
3112
+ "hint": "对数间隔的底数,默认为 2.6。取值范围为 1.0-10.0。",
3035
3113
  "condition": {
3036
3114
  "platform_settings.segmented_reply.interval_method": "log",
3037
3115
  },
3038
3116
  },
3039
3117
  "platform_settings.segmented_reply.words_count_threshold": {
3040
3118
  "description": "分段回复字数阈值",
3119
+ "hint": "分段回复的字数上限。只有字数小于此值的消息才会被分段,超过此值的长消息将直接发送(不分段)。默认为 150",
3041
3120
  "type": "int",
3042
3121
  },
3043
3122
  "platform_settings.segmented_reply.split_mode": {
@@ -3048,6 +3127,7 @@ CONFIG_METADATA_3 = {
3048
3127
  },
3049
3128
  "platform_settings.segmented_reply.regex": {
3050
3129
  "description": "分段正则表达式",
3130
+ "hint": "用于分隔一段消息。默认情况下会根据句号、问号等标点符号分隔。如填写 `[。?!]` 将移除所有的句号、问号、感叹号。re.findall(r'<regex>', text)",
3051
3131
  "type": "string",
3052
3132
  "condition": {
3053
3133
  "platform_settings.segmented_reply.split_mode": "regex",
@@ -3217,6 +3297,7 @@ DEFAULT_VALUE_MAP = {
3217
3297
  "string": "",
3218
3298
  "text": "",
3219
3299
  "list": [],
3300
+ "file": [],
3220
3301
  "object": {},
3221
3302
  "template_list": [],
3222
3303
  }
@@ -14,6 +14,7 @@ from astrbot.core.db.po import (
14
14
  CommandConflict,
15
15
  ConversationV2,
16
16
  Persona,
17
+ PersonaFolder,
17
18
  PlatformMessageHistory,
18
19
  PlatformSession,
19
20
  PlatformStat,
@@ -253,8 +254,21 @@ class BaseDatabase(abc.ABC):
253
254
  system_prompt: str,
254
255
  begin_dialogs: list[str] | None = None,
255
256
  tools: list[str] | None = None,
257
+ skills: list[str] | None = None,
258
+ folder_id: str | None = None,
259
+ sort_order: int = 0,
256
260
  ) -> Persona:
257
- """Insert a new persona record."""
261
+ """Insert a new persona record.
262
+
263
+ Args:
264
+ persona_id: Unique identifier for the persona
265
+ system_prompt: System prompt for the persona
266
+ begin_dialogs: Optional list of initial dialog strings
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)
269
+ folder_id: Optional folder ID to place the persona in (None means root)
270
+ sort_order: Sort order within the folder (default 0)
271
+ """
258
272
  ...
259
273
 
260
274
  @abc.abstractmethod
@@ -274,6 +288,7 @@ class BaseDatabase(abc.ABC):
274
288
  system_prompt: str | None = None,
275
289
  begin_dialogs: list[str] | None = None,
276
290
  tools: list[str] | None = None,
291
+ skills: list[str] | None = None,
277
292
  ) -> Persona | None:
278
293
  """Update a persona's system prompt or begin dialogs."""
279
294
  ...
@@ -283,6 +298,84 @@ class BaseDatabase(abc.ABC):
283
298
  """Delete a persona by its ID."""
284
299
  ...
285
300
 
301
+ # ====
302
+ # Persona Folder Management
303
+ # ====
304
+
305
+ @abc.abstractmethod
306
+ async def insert_persona_folder(
307
+ self,
308
+ name: str,
309
+ parent_id: str | None = None,
310
+ description: str | None = None,
311
+ sort_order: int = 0,
312
+ ) -> PersonaFolder:
313
+ """Insert a new persona folder."""
314
+ ...
315
+
316
+ @abc.abstractmethod
317
+ async def get_persona_folder_by_id(self, folder_id: str) -> PersonaFolder | None:
318
+ """Get a persona folder by its folder_id."""
319
+ ...
320
+
321
+ @abc.abstractmethod
322
+ async def get_persona_folders(
323
+ self, parent_id: str | None = None
324
+ ) -> list[PersonaFolder]:
325
+ """Get all persona folders, optionally filtered by parent_id."""
326
+ ...
327
+
328
+ @abc.abstractmethod
329
+ async def get_all_persona_folders(self) -> list[PersonaFolder]:
330
+ """Get all persona folders."""
331
+ ...
332
+
333
+ @abc.abstractmethod
334
+ async def update_persona_folder(
335
+ self,
336
+ folder_id: str,
337
+ name: str | None = None,
338
+ parent_id: T.Any = None,
339
+ description: T.Any = None,
340
+ sort_order: int | None = None,
341
+ ) -> PersonaFolder | None:
342
+ """Update a persona folder."""
343
+ ...
344
+
345
+ @abc.abstractmethod
346
+ async def delete_persona_folder(self, folder_id: str) -> None:
347
+ """Delete a persona folder by its folder_id."""
348
+ ...
349
+
350
+ @abc.abstractmethod
351
+ async def move_persona_to_folder(
352
+ self, persona_id: str, folder_id: str | None
353
+ ) -> Persona | None:
354
+ """Move a persona to a folder (or root if folder_id is None)."""
355
+ ...
356
+
357
+ @abc.abstractmethod
358
+ async def get_personas_by_folder(
359
+ self, folder_id: str | None = None
360
+ ) -> list[Persona]:
361
+ """Get all personas in a specific folder."""
362
+ ...
363
+
364
+ @abc.abstractmethod
365
+ async def batch_update_sort_order(
366
+ self,
367
+ items: list[dict],
368
+ ) -> None:
369
+ """Batch update sort_order for personas and/or folders.
370
+
371
+ Args:
372
+ items: List of dicts with keys:
373
+ - id: The persona_id or folder_id
374
+ - type: Either "persona" or "folder"
375
+ - sort_order: The new sort_order value
376
+ """
377
+ ...
378
+
286
379
  @abc.abstractmethod
287
380
  async def insert_preference_or_update(
288
381
  self,
astrbot/core/db/po.py CHANGED
@@ -68,6 +68,44 @@ class ConversationV2(SQLModel, table=True):
68
68
  )
69
69
 
70
70
 
71
+ class PersonaFolder(SQLModel, table=True):
72
+ """Persona 文件夹,支持递归层级结构。
73
+
74
+ 用于组织和管理多个 Persona,类似于文件系统的目录结构。
75
+ """
76
+
77
+ __tablename__: str = "persona_folders"
78
+
79
+ id: int | None = Field(
80
+ primary_key=True,
81
+ sa_column_kwargs={"autoincrement": True},
82
+ default=None,
83
+ )
84
+ folder_id: str = Field(
85
+ max_length=36,
86
+ nullable=False,
87
+ unique=True,
88
+ default_factory=lambda: str(uuid.uuid4()),
89
+ )
90
+ name: str = Field(max_length=255, nullable=False)
91
+ parent_id: str | None = Field(default=None, max_length=36)
92
+ """父文件夹ID,NULL表示根目录"""
93
+ description: str | None = Field(default=None, sa_type=Text)
94
+ sort_order: int = Field(default=0)
95
+ created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
96
+ updated_at: datetime = Field(
97
+ default_factory=lambda: datetime.now(timezone.utc),
98
+ sa_column_kwargs={"onupdate": datetime.now(timezone.utc)},
99
+ )
100
+
101
+ __table_args__ = (
102
+ UniqueConstraint(
103
+ "folder_id",
104
+ name="uix_persona_folder_id",
105
+ ),
106
+ )
107
+
108
+
71
109
  class Persona(SQLModel, table=True):
72
110
  """Persona is a set of instructions for LLMs to follow.
73
111
 
@@ -87,6 +125,12 @@ class Persona(SQLModel, table=True):
87
125
  """a list of strings, each representing a dialog to start with"""
88
126
  tools: list | None = Field(default=None, sa_type=JSON)
89
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."""
130
+ folder_id: str | None = Field(default=None, max_length=36)
131
+ """所属文件夹ID,NULL 表示在根目录"""
132
+ sort_order: int = Field(default=0)
133
+ """排序顺序"""
90
134
  created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
91
135
  updated_at: datetime = Field(
92
136
  default_factory=lambda: datetime.now(timezone.utc),
@@ -400,6 +444,8 @@ class Personality(TypedDict):
400
444
  """情感模拟对话预设。在 v4.0.0 版本及之后,已被废弃。"""
401
445
  tools: list[str] | None
402
446
  """工具列表。None 表示使用所有工具,空列表表示不使用任何工具"""
447
+ skills: list[str] | None
448
+ """Skills 列表。None 表示使用所有 Skills,空列表表示不使用任何 Skills"""
403
449
 
404
450
  # cache
405
451
  _begin_dialogs_processed: list[dict]