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.
- astrbot/builtin_stars/astrbot/process_llm_request.py +42 -1
- astrbot/builtin_stars/builtin_commands/commands/__init__.py +0 -2
- astrbot/builtin_stars/builtin_commands/commands/persona.py +68 -6
- astrbot/builtin_stars/builtin_commands/main.py +0 -26
- 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_hooks.py +3 -1
- astrbot/core/astr_agent_run_util.py +243 -1
- 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 +90 -9
- astrbot/core/db/__init__.py +94 -1
- astrbot/core/db/po.py +46 -0
- astrbot/core/db/sqlite.py +248 -0
- astrbot/core/message/components.py +2 -2
- astrbot/core/persona_mgr.py +162 -2
- 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 +73 -6
- astrbot/core/pipeline/process_stage/utils.py +31 -4
- astrbot/core/pipeline/scheduler.py +1 -1
- astrbot/core/pipeline/waking_check/stage.py +0 -1
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +3 -3
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +32 -14
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +61 -2
- astrbot/core/platform/sources/dingtalk/dingtalk_event.py +57 -11
- astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +5 -7
- astrbot/core/platform/sources/webchat/webchat_adapter.py +1 -0
- astrbot/core/platform/sources/webchat/webchat_event.py +24 -0
- astrbot/core/provider/manager.py +38 -0
- astrbot/core/provider/provider.py +54 -0
- astrbot/core/provider/sources/gemini_embedding_source.py +1 -1
- astrbot/core/provider/sources/gemini_source.py +12 -9
- astrbot/core/provider/sources/genie_tts.py +128 -0
- astrbot/core/provider/sources/openai_embedding_source.py +1 -1
- 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/context.py +9 -8
- astrbot/core/star/filter/command.py +1 -1
- astrbot/core/star/filter/custom_filter.py +2 -2
- astrbot/core/star/register/star_handler.py +2 -4
- 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/live_chat.py +423 -0
- astrbot/dashboard/routes/persona.py +265 -1
- astrbot/dashboard/routes/skills.py +148 -0
- astrbot/dashboard/routes/util.py +102 -0
- astrbot/dashboard/server.py +21 -5
- {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/METADATA +1 -1
- {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/RECORD +69 -63
- astrbot/builtin_stars/builtin_commands/commands/tool.py +0 -31
- 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.3.dist-info → astrbot-4.13.0.dist-info}/WHEEL +0 -0
- {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/entry_points.txt +0 -0
- {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,
|
|
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
|
@@ -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。
|
|
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,
|
|
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("/")
|