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
astrbot/core/db/sqlite.py CHANGED
@@ -16,6 +16,7 @@ from astrbot.core.db.po import (
16
16
  CommandConflict,
17
17
  ConversationV2,
18
18
  Persona,
19
+ PersonaFolder,
19
20
  PlatformMessageHistory,
20
21
  PlatformSession,
21
22
  PlatformStat,
@@ -51,8 +52,43 @@ class SQLiteDatabase(BaseDatabase):
51
52
  await conn.execute(text("PRAGMA temp_store=MEMORY"))
52
53
  await conn.execute(text("PRAGMA mmap_size=134217728"))
53
54
  await conn.execute(text("PRAGMA optimize"))
55
+ # 确保 personas 表有 folder_id、sort_order、skills 列(前向兼容)
56
+ await self._ensure_persona_folder_columns(conn)
57
+ await self._ensure_persona_skills_column(conn)
54
58
  await conn.commit()
55
59
 
60
+ async def _ensure_persona_folder_columns(self, conn) -> None:
61
+ """确保 personas 表有 folder_id 和 sort_order 列。
62
+
63
+ 这是为了支持旧版数据库的平滑升级。新版数据库通过 SQLModel
64
+ 的 metadata.create_all 自动创建这些列。
65
+ """
66
+ result = await conn.execute(text("PRAGMA table_info(personas)"))
67
+ columns = {row[1] for row in result.fetchall()}
68
+
69
+ if "folder_id" not in columns:
70
+ await conn.execute(
71
+ text(
72
+ "ALTER TABLE personas ADD COLUMN folder_id VARCHAR(36) DEFAULT NULL"
73
+ )
74
+ )
75
+ if "sort_order" not in columns:
76
+ await conn.execute(
77
+ text("ALTER TABLE personas ADD COLUMN sort_order INTEGER DEFAULT 0")
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
+
56
92
  # ====
57
93
  # Platform Statistics
58
94
  # ====
@@ -541,6 +577,9 @@ class SQLiteDatabase(BaseDatabase):
541
577
  system_prompt,
542
578
  begin_dialogs=None,
543
579
  tools=None,
580
+ skills=None,
581
+ folder_id=None,
582
+ sort_order=0,
544
583
  ):
545
584
  """Insert a new persona record."""
546
585
  async with self.get_db() as session:
@@ -551,8 +590,13 @@ class SQLiteDatabase(BaseDatabase):
551
590
  system_prompt=system_prompt,
552
591
  begin_dialogs=begin_dialogs or [],
553
592
  tools=tools,
593
+ skills=skills,
594
+ folder_id=folder_id,
595
+ sort_order=sort_order,
554
596
  )
555
597
  session.add(new_persona)
598
+ await session.flush()
599
+ await session.refresh(new_persona)
556
600
  return new_persona
557
601
 
558
602
  async def get_persona_by_id(self, persona_id):
@@ -577,6 +621,7 @@ class SQLiteDatabase(BaseDatabase):
577
621
  system_prompt=None,
578
622
  begin_dialogs=None,
579
623
  tools=NOT_GIVEN,
624
+ skills=NOT_GIVEN,
580
625
  ):
581
626
  """Update a persona's system prompt or begin dialogs."""
582
627
  async with self.get_db() as session:
@@ -590,6 +635,8 @@ class SQLiteDatabase(BaseDatabase):
590
635
  values["begin_dialogs"] = begin_dialogs
591
636
  if tools is not NOT_GIVEN:
592
637
  values["tools"] = tools
638
+ if skills is not NOT_GIVEN:
639
+ values["skills"] = skills
593
640
  if not values:
594
641
  return None
595
642
  query = query.values(**values)
@@ -605,6 +652,207 @@ class SQLiteDatabase(BaseDatabase):
605
652
  delete(Persona).where(col(Persona.persona_id) == persona_id),
606
653
  )
607
654
 
655
+ # ====
656
+ # Persona Folder Management
657
+ # ====
658
+
659
+ async def insert_persona_folder(
660
+ self,
661
+ name: str,
662
+ parent_id: str | None = None,
663
+ description: str | None = None,
664
+ sort_order: int = 0,
665
+ ) -> PersonaFolder:
666
+ """Insert a new persona folder."""
667
+ async with self.get_db() as session:
668
+ session: AsyncSession
669
+ async with session.begin():
670
+ new_folder = PersonaFolder(
671
+ name=name,
672
+ parent_id=parent_id,
673
+ description=description,
674
+ sort_order=sort_order,
675
+ )
676
+ session.add(new_folder)
677
+ await session.flush()
678
+ await session.refresh(new_folder)
679
+ return new_folder
680
+
681
+ async def get_persona_folder_by_id(self, folder_id: str) -> PersonaFolder | None:
682
+ """Get a persona folder by its folder_id."""
683
+ async with self.get_db() as session:
684
+ session: AsyncSession
685
+ query = select(PersonaFolder).where(PersonaFolder.folder_id == folder_id)
686
+ result = await session.execute(query)
687
+ return result.scalar_one_or_none()
688
+
689
+ async def get_persona_folders(
690
+ self, parent_id: str | None = None
691
+ ) -> list[PersonaFolder]:
692
+ """Get all persona folders, optionally filtered by parent_id.
693
+
694
+ Args:
695
+ parent_id: If None, returns root folders only. If specified, returns
696
+ children of that folder.
697
+ """
698
+ async with self.get_db() as session:
699
+ session: AsyncSession
700
+ if parent_id is None:
701
+ # Get root folders (parent_id is NULL)
702
+ query = (
703
+ select(PersonaFolder)
704
+ .where(col(PersonaFolder.parent_id).is_(None))
705
+ .order_by(col(PersonaFolder.sort_order), col(PersonaFolder.name))
706
+ )
707
+ else:
708
+ query = (
709
+ select(PersonaFolder)
710
+ .where(PersonaFolder.parent_id == parent_id)
711
+ .order_by(col(PersonaFolder.sort_order), col(PersonaFolder.name))
712
+ )
713
+ result = await session.execute(query)
714
+ return list(result.scalars().all())
715
+
716
+ async def get_all_persona_folders(self) -> list[PersonaFolder]:
717
+ """Get all persona folders."""
718
+ async with self.get_db() as session:
719
+ session: AsyncSession
720
+ query = select(PersonaFolder).order_by(
721
+ col(PersonaFolder.sort_order), col(PersonaFolder.name)
722
+ )
723
+ result = await session.execute(query)
724
+ return list(result.scalars().all())
725
+
726
+ async def update_persona_folder(
727
+ self,
728
+ folder_id: str,
729
+ name: str | None = None,
730
+ parent_id: T.Any = NOT_GIVEN,
731
+ description: T.Any = NOT_GIVEN,
732
+ sort_order: int | None = None,
733
+ ) -> PersonaFolder | None:
734
+ """Update a persona folder."""
735
+ async with self.get_db() as session:
736
+ session: AsyncSession
737
+ async with session.begin():
738
+ query = update(PersonaFolder).where(
739
+ col(PersonaFolder.folder_id) == folder_id
740
+ )
741
+ values: dict[str, T.Any] = {}
742
+ if name is not None:
743
+ values["name"] = name
744
+ if parent_id is not NOT_GIVEN:
745
+ values["parent_id"] = parent_id
746
+ if description is not NOT_GIVEN:
747
+ values["description"] = description
748
+ if sort_order is not None:
749
+ values["sort_order"] = sort_order
750
+ if not values:
751
+ return None
752
+ query = query.values(**values)
753
+ await session.execute(query)
754
+ return await self.get_persona_folder_by_id(folder_id)
755
+
756
+ async def delete_persona_folder(self, folder_id: str) -> None:
757
+ """Delete a persona folder by its folder_id.
758
+
759
+ Note: This will also set folder_id to NULL for all personas in this folder,
760
+ moving them to the root directory.
761
+ """
762
+ async with self.get_db() as session:
763
+ session: AsyncSession
764
+ async with session.begin():
765
+ # Move personas to root directory
766
+ await session.execute(
767
+ update(Persona)
768
+ .where(col(Persona.folder_id) == folder_id)
769
+ .values(folder_id=None)
770
+ )
771
+ # Delete the folder
772
+ await session.execute(
773
+ delete(PersonaFolder).where(
774
+ col(PersonaFolder.folder_id) == folder_id
775
+ ),
776
+ )
777
+
778
+ async def move_persona_to_folder(
779
+ self, persona_id: str, folder_id: str | None
780
+ ) -> Persona | None:
781
+ """Move a persona to a folder (or root if folder_id is None)."""
782
+ async with self.get_db() as session:
783
+ session: AsyncSession
784
+ async with session.begin():
785
+ await session.execute(
786
+ update(Persona)
787
+ .where(col(Persona.persona_id) == persona_id)
788
+ .values(folder_id=folder_id)
789
+ )
790
+ return await self.get_persona_by_id(persona_id)
791
+
792
+ async def get_personas_by_folder(
793
+ self, folder_id: str | None = None
794
+ ) -> list[Persona]:
795
+ """Get all personas in a specific folder.
796
+
797
+ Args:
798
+ folder_id: If None, returns personas in root directory.
799
+ """
800
+ async with self.get_db() as session:
801
+ session: AsyncSession
802
+ if folder_id is None:
803
+ query = (
804
+ select(Persona)
805
+ .where(col(Persona.folder_id).is_(None))
806
+ .order_by(col(Persona.sort_order), col(Persona.persona_id))
807
+ )
808
+ else:
809
+ query = (
810
+ select(Persona)
811
+ .where(Persona.folder_id == folder_id)
812
+ .order_by(col(Persona.sort_order), col(Persona.persona_id))
813
+ )
814
+ result = await session.execute(query)
815
+ return list(result.scalars().all())
816
+
817
+ async def batch_update_sort_order(
818
+ self,
819
+ items: list[dict],
820
+ ) -> None:
821
+ """Batch update sort_order for personas and/or folders.
822
+
823
+ Args:
824
+ items: List of dicts with keys:
825
+ - id: The persona_id or folder_id
826
+ - type: Either "persona" or "folder"
827
+ - sort_order: The new sort_order value
828
+ """
829
+ if not items:
830
+ return
831
+
832
+ async with self.get_db() as session:
833
+ session: AsyncSession
834
+ async with session.begin():
835
+ for item in items:
836
+ item_id = item.get("id")
837
+ item_type = item.get("type")
838
+ sort_order = item.get("sort_order")
839
+
840
+ if item_id is None or item_type is None or sort_order is None:
841
+ continue
842
+
843
+ if item_type == "persona":
844
+ await session.execute(
845
+ update(Persona)
846
+ .where(col(Persona.persona_id) == item_id)
847
+ .values(sort_order=sort_order)
848
+ )
849
+ elif item_type == "folder":
850
+ await session.execute(
851
+ update(PersonaFolder)
852
+ .where(col(PersonaFolder.folder_id) == item_id)
853
+ .values(sort_order=sort_order)
854
+ )
855
+
608
856
  async def insert_preference_or_update(self, scope, scope_id, key, value):
609
857
  """Insert a new preference record or update if it exists."""
610
858
  async with self.get_db() as session:
@@ -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, (Image, Record)):
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, (Node, Nodes)):
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)
@@ -1,7 +1,7 @@
1
1
  from astrbot import logger
2
2
  from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
3
3
  from astrbot.core.db import BaseDatabase
4
- from astrbot.core.db.po import Persona, Personality
4
+ from astrbot.core.db.po import Persona, PersonaFolder, Personality
5
5
  from astrbot.core.platform.message_session import MessageSession
6
6
 
7
7
  DEFAULT_PERSONALITY = Personality(
@@ -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):
@@ -94,14 +97,166 @@ class PersonaManager:
94
97
  """获取所有 personas"""
95
98
  return await self.db.get_personas()
96
99
 
100
+ async def get_personas_by_folder(
101
+ self, folder_id: str | None = None
102
+ ) -> list[Persona]:
103
+ """获取指定文件夹中的 personas
104
+
105
+ Args:
106
+ folder_id: 文件夹 ID,None 表示根目录
107
+ """
108
+ return await self.db.get_personas_by_folder(folder_id)
109
+
110
+ async def move_persona_to_folder(
111
+ self, persona_id: str, folder_id: str | None
112
+ ) -> Persona | None:
113
+ """移动 persona 到指定文件夹
114
+
115
+ Args:
116
+ persona_id: Persona ID
117
+ folder_id: 目标文件夹 ID,None 表示移动到根目录
118
+ """
119
+ persona = await self.db.move_persona_to_folder(persona_id, folder_id)
120
+ if persona:
121
+ for i, p in enumerate(self.personas):
122
+ if p.persona_id == persona_id:
123
+ self.personas[i] = persona
124
+ break
125
+ return persona
126
+
127
+ # ====
128
+ # Persona Folder Management
129
+ # ====
130
+
131
+ async def create_folder(
132
+ self,
133
+ name: str,
134
+ parent_id: str | None = None,
135
+ description: str | None = None,
136
+ sort_order: int = 0,
137
+ ) -> PersonaFolder:
138
+ """创建新的文件夹"""
139
+ return await self.db.insert_persona_folder(
140
+ name=name,
141
+ parent_id=parent_id,
142
+ description=description,
143
+ sort_order=sort_order,
144
+ )
145
+
146
+ async def get_folder(self, folder_id: str) -> PersonaFolder | None:
147
+ """获取指定文件夹"""
148
+ return await self.db.get_persona_folder_by_id(folder_id)
149
+
150
+ async def get_folders(self, parent_id: str | None = None) -> list[PersonaFolder]:
151
+ """获取文件夹列表
152
+
153
+ Args:
154
+ parent_id: 父文件夹 ID,None 表示获取根目录下的文件夹
155
+ """
156
+ return await self.db.get_persona_folders(parent_id)
157
+
158
+ async def get_all_folders(self) -> list[PersonaFolder]:
159
+ """获取所有文件夹"""
160
+ return await self.db.get_all_persona_folders()
161
+
162
+ async def update_folder(
163
+ self,
164
+ folder_id: str,
165
+ name: str | None = None,
166
+ parent_id: str | None = None,
167
+ description: str | None = None,
168
+ sort_order: int | None = None,
169
+ ) -> PersonaFolder | None:
170
+ """更新文件夹信息"""
171
+ return await self.db.update_persona_folder(
172
+ folder_id=folder_id,
173
+ name=name,
174
+ parent_id=parent_id,
175
+ description=description,
176
+ sort_order=sort_order,
177
+ )
178
+
179
+ async def delete_folder(self, folder_id: str) -> None:
180
+ """删除文件夹
181
+
182
+ Note: 文件夹内的 personas 会被移动到根目录
183
+ """
184
+ await self.db.delete_persona_folder(folder_id)
185
+
186
+ async def batch_update_sort_order(self, items: list[dict]) -> None:
187
+ """批量更新 personas 和/或 folders 的排序顺序
188
+
189
+ Args:
190
+ items: 包含以下键的字典列表:
191
+ - id: persona_id 或 folder_id
192
+ - type: "persona" 或 "folder"
193
+ - sort_order: 新的排序顺序值
194
+ """
195
+ await self.db.batch_update_sort_order(items)
196
+ # 刷新缓存
197
+ self.personas = await self.get_all_personas()
198
+ self.get_v3_persona_data()
199
+
200
+ async def get_folder_tree(self) -> list[dict]:
201
+ """获取文件夹树形结构
202
+
203
+ Returns:
204
+ 树形结构的文件夹列表,每个文件夹包含 children 子列表
205
+ """
206
+ all_folders = await self.get_all_folders()
207
+ folder_map: dict[str, dict] = {}
208
+
209
+ # 创建文件夹字典
210
+ for folder in all_folders:
211
+ folder_map[folder.folder_id] = {
212
+ "folder_id": folder.folder_id,
213
+ "name": folder.name,
214
+ "parent_id": folder.parent_id,
215
+ "description": folder.description,
216
+ "sort_order": folder.sort_order,
217
+ "children": [],
218
+ }
219
+
220
+ # 构建树形结构
221
+ root_folders = []
222
+ for folder_id, folder_data in folder_map.items():
223
+ parent_id = folder_data["parent_id"]
224
+ if parent_id is None:
225
+ root_folders.append(folder_data)
226
+ elif parent_id in folder_map:
227
+ folder_map[parent_id]["children"].append(folder_data)
228
+
229
+ # 递归排序
230
+ def sort_folders(folders: list[dict]) -> list[dict]:
231
+ folders.sort(key=lambda f: (f["sort_order"], f["name"]))
232
+ for folder in folders:
233
+ if folder["children"]:
234
+ folder["children"] = sort_folders(folder["children"])
235
+ return folders
236
+
237
+ return sort_folders(root_folders)
238
+
97
239
  async def create_persona(
98
240
  self,
99
241
  persona_id: str,
100
242
  system_prompt: str,
101
243
  begin_dialogs: list[str] | None = None,
102
244
  tools: list[str] | None = None,
245
+ skills: list[str] | None = None,
246
+ folder_id: str | None = None,
247
+ sort_order: int = 0,
103
248
  ) -> Persona:
104
- """创建新的 persona。tools 参数为 None 时表示使用所有工具,空列表表示不使用任何工具"""
249
+ """创建新的 persona。
250
+
251
+ Args:
252
+ persona_id: Persona 唯一标识
253
+ system_prompt: 系统提示词
254
+ begin_dialogs: 预设对话列表
255
+ tools: 工具列表,None 表示使用所有工具,空列表表示不使用任何工具
256
+ skills: Skills 列表,None 表示使用所有 Skills,空列表表示不使用任何 Skills
257
+ folder_id: 所属文件夹 ID,None 表示根目录
258
+ sort_order: 排序顺序
259
+ """
105
260
  if await self.db.get_persona_by_id(persona_id):
106
261
  raise ValueError(f"Persona with ID {persona_id} already exists.")
107
262
  new_persona = await self.db.insert_persona(
@@ -109,6 +264,9 @@ class PersonaManager:
109
264
  system_prompt,
110
265
  begin_dialogs,
111
266
  tools=tools,
267
+ skills=skills,
268
+ folder_id=folder_id,
269
+ sort_order=sort_order,
112
270
  )
113
271
  self.personas.append(new_persona)
114
272
  self.get_v3_persona_data()
@@ -132,6 +290,7 @@ class PersonaManager:
132
290
  "begin_dialogs": persona.begin_dialogs or [],
133
291
  "mood_imitation_dialogs": [], # deprecated
134
292
  "tools": persona.tools,
293
+ "skills": persona.skills,
135
294
  }
136
295
  for persona in self.personas
137
296
  ]
@@ -187,6 +346,7 @@ class PersonaManager:
187
346
  system_prompt=selected_default_persona["prompt"],
188
347
  begin_dialogs=selected_default_persona["begin_dialogs"],
189
348
  tools=selected_default_persona["tools"] or None,
349
+ skills=selected_default_persona["skills"] or None,
190
350
  )
191
351
 
192
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, (MessageEventResult, CommandResult)):
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, (MessageEventResult, CommandResult)):
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, (Record, Image)) and component.url:
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("/")