AstrBot 4.12.2__py3-none-any.whl → 4.12.4__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/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/astr_agent_hooks.py +5 -3
- astrbot/core/astr_agent_run_util.py +243 -1
- astrbot/core/config/default.py +30 -1
- astrbot/core/db/__init__.py +91 -1
- astrbot/core/db/po.py +42 -0
- astrbot/core/db/sqlite.py +230 -0
- astrbot/core/persona_mgr.py +154 -2
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +57 -4
- astrbot/core/pipeline/process_stage/utils.py +13 -1
- astrbot/core/pipeline/waking_check/stage.py +0 -1
- 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/webchat/webchat_adapter.py +1 -0
- astrbot/core/platform/sources/webchat/webchat_event.py +24 -0
- astrbot/core/provider/manager.py +7 -0
- astrbot/core/provider/provider.py +54 -0
- astrbot/core/provider/sources/gemini_embedding_source.py +1 -1
- astrbot/core/provider/sources/genie_tts.py +128 -0
- astrbot/core/provider/sources/openai_embedding_source.py +1 -1
- astrbot/core/star/context.py +9 -8
- astrbot/core/star/register/star_handler.py +2 -4
- astrbot/core/star/star_handler.py +2 -1
- astrbot/dashboard/routes/live_chat.py +423 -0
- astrbot/dashboard/routes/persona.py +258 -1
- astrbot/dashboard/server.py +2 -0
- {astrbot-4.12.2.dist-info → astrbot-4.12.4.dist-info}/METADATA +1 -1
- {astrbot-4.12.2.dist-info → astrbot-4.12.4.dist-info}/RECORD +35 -34
- astrbot/builtin_stars/builtin_commands/commands/tool.py +0 -31
- {astrbot-4.12.2.dist-info → astrbot-4.12.4.dist-info}/WHEEL +0 -0
- {astrbot-4.12.2.dist-info → astrbot-4.12.4.dist-info}/entry_points.txt +0 -0
- {astrbot-4.12.2.dist-info → astrbot-4.12.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -11,7 +11,6 @@ from .provider import ProviderCommands
|
|
|
11
11
|
from .setunset import SetUnsetCommands
|
|
12
12
|
from .sid import SIDCommand
|
|
13
13
|
from .t2i import T2ICommand
|
|
14
|
-
from .tool import ToolCommands
|
|
15
14
|
from .tts import TTSCommand
|
|
16
15
|
|
|
17
16
|
__all__ = [
|
|
@@ -27,5 +26,4 @@ __all__ = [
|
|
|
27
26
|
"SetUnsetCommands",
|
|
28
27
|
"T2ICommand",
|
|
29
28
|
"TTSCommand",
|
|
30
|
-
"ToolCommands",
|
|
31
29
|
]
|
|
@@ -1,13 +1,55 @@
|
|
|
1
1
|
import builtins
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
2
3
|
|
|
3
4
|
from astrbot.api import sp, star
|
|
4
5
|
from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
|
5
6
|
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from astrbot.core.db.po import Persona
|
|
9
|
+
|
|
6
10
|
|
|
7
11
|
class PersonaCommands:
|
|
8
12
|
def __init__(self, context: star.Context):
|
|
9
13
|
self.context = context
|
|
10
14
|
|
|
15
|
+
def _build_tree_output(
|
|
16
|
+
self,
|
|
17
|
+
folder_tree: list[dict],
|
|
18
|
+
all_personas: list["Persona"],
|
|
19
|
+
depth: int = 0,
|
|
20
|
+
) -> list[str]:
|
|
21
|
+
"""递归构建树状输出,使用短线条表示层级"""
|
|
22
|
+
lines: list[str] = []
|
|
23
|
+
# 使用短线条作为缩进前缀,每层只用 "│" 加一个空格
|
|
24
|
+
prefix = "│ " * depth
|
|
25
|
+
|
|
26
|
+
for folder in folder_tree:
|
|
27
|
+
# 输出文件夹
|
|
28
|
+
lines.append(f"{prefix}├ 📁 {folder['name']}/")
|
|
29
|
+
|
|
30
|
+
# 获取该文件夹下的人格
|
|
31
|
+
folder_personas = [
|
|
32
|
+
p for p in all_personas if p.folder_id == folder["folder_id"]
|
|
33
|
+
]
|
|
34
|
+
child_prefix = "│ " * (depth + 1)
|
|
35
|
+
|
|
36
|
+
# 输出该文件夹下的人格
|
|
37
|
+
for persona in folder_personas:
|
|
38
|
+
lines.append(f"{child_prefix}├ 👤 {persona.persona_id}")
|
|
39
|
+
|
|
40
|
+
# 递归处理子文件夹
|
|
41
|
+
children = folder.get("children", [])
|
|
42
|
+
if children:
|
|
43
|
+
lines.extend(
|
|
44
|
+
self._build_tree_output(
|
|
45
|
+
children,
|
|
46
|
+
all_personas,
|
|
47
|
+
depth + 1,
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return lines
|
|
52
|
+
|
|
11
53
|
async def persona(self, message: AstrMessageEvent):
|
|
12
54
|
l = message.message_str.split(" ") # noqa: E741
|
|
13
55
|
umo = message.unified_msg_origin
|
|
@@ -69,12 +111,32 @@ class PersonaCommands:
|
|
|
69
111
|
.use_t2i(False),
|
|
70
112
|
)
|
|
71
113
|
elif l[1] == "list":
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
114
|
+
# 获取文件夹树和所有人格
|
|
115
|
+
folder_tree = await self.context.persona_manager.get_folder_tree()
|
|
116
|
+
all_personas = self.context.persona_manager.personas
|
|
117
|
+
|
|
118
|
+
lines = ["📂 人格列表:\n"]
|
|
119
|
+
|
|
120
|
+
# 构建树状输出
|
|
121
|
+
tree_lines = self._build_tree_output(folder_tree, all_personas)
|
|
122
|
+
lines.extend(tree_lines)
|
|
123
|
+
|
|
124
|
+
# 输出根目录下的人格(没有文件夹的)
|
|
125
|
+
root_personas = [p for p in all_personas if p.folder_id is None]
|
|
126
|
+
if root_personas:
|
|
127
|
+
if tree_lines: # 如果有文件夹内容,加个空行
|
|
128
|
+
lines.append("")
|
|
129
|
+
for persona in root_personas:
|
|
130
|
+
lines.append(f"👤 {persona.persona_id}")
|
|
131
|
+
|
|
132
|
+
# 统计信息
|
|
133
|
+
total_count = len(all_personas)
|
|
134
|
+
lines.append(f"\n共 {total_count} 个人格")
|
|
135
|
+
lines.append("\n*使用 `/persona <人格名>` 设置人格")
|
|
136
|
+
lines.append("*使用 `/persona view <人格名>` 查看详细信息")
|
|
137
|
+
|
|
138
|
+
msg = "\n".join(lines)
|
|
139
|
+
message.set_result(MessageEventResult().message(msg).use_t2i(False))
|
|
78
140
|
elif l[1] == "view":
|
|
79
141
|
if len(l) == 2:
|
|
80
142
|
message.set_result(MessageEventResult().message("请输入人格情景名"))
|
|
@@ -13,7 +13,6 @@ from .commands import (
|
|
|
13
13
|
SetUnsetCommands,
|
|
14
14
|
SIDCommand,
|
|
15
15
|
T2ICommand,
|
|
16
|
-
ToolCommands,
|
|
17
16
|
TTSCommand,
|
|
18
17
|
)
|
|
19
18
|
|
|
@@ -24,7 +23,6 @@ class Main(star.Star):
|
|
|
24
23
|
|
|
25
24
|
self.help_c = HelpCommand(self.context)
|
|
26
25
|
self.llm_c = LLMCommands(self.context)
|
|
27
|
-
self.tool_c = ToolCommands(self.context)
|
|
28
26
|
self.plugin_c = PluginCommands(self.context)
|
|
29
27
|
self.admin_c = AdminCommands(self.context)
|
|
30
28
|
self.conversation_c = ConversationCommands(self.context)
|
|
@@ -47,30 +45,6 @@ class Main(star.Star):
|
|
|
47
45
|
"""开启/关闭 LLM"""
|
|
48
46
|
await self.llm_c.llm(event)
|
|
49
47
|
|
|
50
|
-
@filter.command_group("tool")
|
|
51
|
-
def tool(self):
|
|
52
|
-
"""函数工具管理"""
|
|
53
|
-
|
|
54
|
-
@tool.command("ls")
|
|
55
|
-
async def tool_ls(self, event: AstrMessageEvent):
|
|
56
|
-
"""查看函数工具列表"""
|
|
57
|
-
await self.tool_c.tool_ls(event)
|
|
58
|
-
|
|
59
|
-
@tool.command("on")
|
|
60
|
-
async def tool_on(self, event: AstrMessageEvent, tool_name: str):
|
|
61
|
-
"""启用一个函数工具"""
|
|
62
|
-
await self.tool_c.tool_on(event, tool_name)
|
|
63
|
-
|
|
64
|
-
@tool.command("off")
|
|
65
|
-
async def tool_off(self, event: AstrMessageEvent, tool_name: str):
|
|
66
|
-
"""停用一个函数工具"""
|
|
67
|
-
await self.tool_c.tool_off(event, tool_name)
|
|
68
|
-
|
|
69
|
-
@tool.command("off_all")
|
|
70
|
-
async def tool_all_off(self, event: AstrMessageEvent):
|
|
71
|
-
"""停用所有函数工具"""
|
|
72
|
-
await self.tool_c.tool_all_off(event)
|
|
73
|
-
|
|
74
48
|
@filter.command_group("plugin")
|
|
75
49
|
def plugin(self):
|
|
76
50
|
"""插件管理"""
|
astrbot/cli/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "4.12.
|
|
1
|
+
__version__ = "4.12.4"
|
astrbot/core/astr_agent_hooks.py
CHANGED
|
@@ -34,7 +34,7 @@ class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
|
|
|
34
34
|
):
|
|
35
35
|
await call_event_hook(
|
|
36
36
|
run_context.context.event,
|
|
37
|
-
EventType.
|
|
37
|
+
EventType.OnUsingLLMToolEvent,
|
|
38
38
|
tool,
|
|
39
39
|
tool_args,
|
|
40
40
|
)
|
|
@@ -49,15 +49,17 @@ class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
|
|
|
49
49
|
run_context.context.event.clear_result()
|
|
50
50
|
await call_event_hook(
|
|
51
51
|
run_context.context.event,
|
|
52
|
-
EventType.
|
|
52
|
+
EventType.OnLLMToolRespondEvent,
|
|
53
53
|
tool,
|
|
54
54
|
tool_args,
|
|
55
55
|
tool_result,
|
|
56
56
|
)
|
|
57
57
|
|
|
58
58
|
# special handle web_search_tavily
|
|
59
|
+
platform_name = run_context.context.event.get_platform_name()
|
|
59
60
|
if (
|
|
60
|
-
|
|
61
|
+
platform_name == "webchat"
|
|
62
|
+
and tool.name == "web_search_tavily"
|
|
61
63
|
and len(run_context.messages) > 0
|
|
62
64
|
and tool_result
|
|
63
65
|
and len(tool_result.content)
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import re
|
|
3
|
+
import time
|
|
1
4
|
import traceback
|
|
2
5
|
from collections.abc import AsyncGenerator
|
|
3
6
|
|
|
@@ -5,13 +8,14 @@ from astrbot.core import logger
|
|
|
5
8
|
from astrbot.core.agent.message import Message
|
|
6
9
|
from astrbot.core.agent.runners.tool_loop_agent_runner import ToolLoopAgentRunner
|
|
7
10
|
from astrbot.core.astr_agent_context import AstrAgentContext
|
|
8
|
-
from astrbot.core.message.components import Json
|
|
11
|
+
from astrbot.core.message.components import BaseMessageComponent, Json, Plain
|
|
9
12
|
from astrbot.core.message.message_event_result import (
|
|
10
13
|
MessageChain,
|
|
11
14
|
MessageEventResult,
|
|
12
15
|
ResultContentType,
|
|
13
16
|
)
|
|
14
17
|
from astrbot.core.provider.entities import LLMResponse
|
|
18
|
+
from astrbot.core.provider.provider import TTSProvider
|
|
15
19
|
|
|
16
20
|
AgentRunner = ToolLoopAgentRunner[AstrAgentContext]
|
|
17
21
|
|
|
@@ -131,3 +135,241 @@ async def run_agent(
|
|
|
131
135
|
else:
|
|
132
136
|
astr_event.set_result(MessageEventResult().message(err_msg))
|
|
133
137
|
return
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
async def run_live_agent(
|
|
141
|
+
agent_runner: AgentRunner,
|
|
142
|
+
tts_provider: TTSProvider | None = None,
|
|
143
|
+
max_step: int = 30,
|
|
144
|
+
show_tool_use: bool = True,
|
|
145
|
+
show_reasoning: bool = False,
|
|
146
|
+
) -> AsyncGenerator[MessageChain | None, None]:
|
|
147
|
+
"""Live Mode 的 Agent 运行器,支持流式 TTS
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
agent_runner: Agent 运行器
|
|
151
|
+
tts_provider: TTS Provider 实例
|
|
152
|
+
max_step: 最大步数
|
|
153
|
+
show_tool_use: 是否显示工具使用
|
|
154
|
+
show_reasoning: 是否显示推理过程
|
|
155
|
+
|
|
156
|
+
Yields:
|
|
157
|
+
MessageChain: 包含文本或音频数据的消息链
|
|
158
|
+
"""
|
|
159
|
+
# 如果没有 TTS Provider,直接发送文本
|
|
160
|
+
if not tts_provider:
|
|
161
|
+
async for chain in run_agent(
|
|
162
|
+
agent_runner,
|
|
163
|
+
max_step=max_step,
|
|
164
|
+
show_tool_use=show_tool_use,
|
|
165
|
+
stream_to_general=False,
|
|
166
|
+
show_reasoning=show_reasoning,
|
|
167
|
+
):
|
|
168
|
+
yield chain
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
support_stream = tts_provider.support_stream()
|
|
172
|
+
if support_stream:
|
|
173
|
+
logger.info("[Live Agent] 使用流式 TTS(原生支持 get_audio_stream)")
|
|
174
|
+
else:
|
|
175
|
+
logger.info(
|
|
176
|
+
f"[Live Agent] 使用 TTS({tts_provider.meta().type} "
|
|
177
|
+
"使用 get_audio,将按句子分块生成音频)"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# 统计数据初始化
|
|
181
|
+
tts_start_time = time.time()
|
|
182
|
+
tts_first_frame_time = 0.0
|
|
183
|
+
first_chunk_received = False
|
|
184
|
+
|
|
185
|
+
# 创建队列
|
|
186
|
+
text_queue: asyncio.Queue[str | None] = asyncio.Queue()
|
|
187
|
+
# audio_queue stored bytes or (text, bytes)
|
|
188
|
+
audio_queue: asyncio.Queue[bytes | tuple[str, bytes] | None] = asyncio.Queue()
|
|
189
|
+
|
|
190
|
+
# 1. 启动 Agent Feeder 任务:负责运行 Agent 并将文本分句喂给 text_queue
|
|
191
|
+
feeder_task = asyncio.create_task(
|
|
192
|
+
_run_agent_feeder(
|
|
193
|
+
agent_runner, text_queue, max_step, show_tool_use, show_reasoning
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# 2. 启动 TTS 任务:负责从 text_queue 读取文本并生成音频到 audio_queue
|
|
198
|
+
if support_stream:
|
|
199
|
+
tts_task = asyncio.create_task(
|
|
200
|
+
_safe_tts_stream_wrapper(tts_provider, text_queue, audio_queue)
|
|
201
|
+
)
|
|
202
|
+
else:
|
|
203
|
+
tts_task = asyncio.create_task(
|
|
204
|
+
_simulated_stream_tts(tts_provider, text_queue, audio_queue)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# 3. 主循环:从 audio_queue 读取音频并 yield
|
|
208
|
+
try:
|
|
209
|
+
while True:
|
|
210
|
+
queue_item = await audio_queue.get()
|
|
211
|
+
|
|
212
|
+
if queue_item is None:
|
|
213
|
+
break
|
|
214
|
+
|
|
215
|
+
text = None
|
|
216
|
+
if isinstance(queue_item, tuple):
|
|
217
|
+
text, audio_data = queue_item
|
|
218
|
+
else:
|
|
219
|
+
audio_data = queue_item
|
|
220
|
+
|
|
221
|
+
if not first_chunk_received:
|
|
222
|
+
# 记录首帧延迟(从开始处理到收到第一个音频块)
|
|
223
|
+
tts_first_frame_time = time.time() - tts_start_time
|
|
224
|
+
first_chunk_received = True
|
|
225
|
+
|
|
226
|
+
# 将音频数据封装为 MessageChain
|
|
227
|
+
import base64
|
|
228
|
+
|
|
229
|
+
audio_b64 = base64.b64encode(audio_data).decode("utf-8")
|
|
230
|
+
comps: list[BaseMessageComponent] = [Plain(audio_b64)]
|
|
231
|
+
if text:
|
|
232
|
+
comps.append(Json(data={"text": text}))
|
|
233
|
+
chain = MessageChain(chain=comps, type="audio_chunk")
|
|
234
|
+
yield chain
|
|
235
|
+
|
|
236
|
+
except Exception as e:
|
|
237
|
+
logger.error(f"[Live Agent] 运行时发生错误: {e}", exc_info=True)
|
|
238
|
+
finally:
|
|
239
|
+
# 清理任务
|
|
240
|
+
if not feeder_task.done():
|
|
241
|
+
feeder_task.cancel()
|
|
242
|
+
if not tts_task.done():
|
|
243
|
+
tts_task.cancel()
|
|
244
|
+
|
|
245
|
+
# 确保队列被消费
|
|
246
|
+
pass
|
|
247
|
+
|
|
248
|
+
tts_end_time = time.time()
|
|
249
|
+
|
|
250
|
+
# 发送 TTS 统计信息
|
|
251
|
+
try:
|
|
252
|
+
astr_event = agent_runner.run_context.context.event
|
|
253
|
+
if astr_event.get_platform_name() == "webchat":
|
|
254
|
+
tts_duration = tts_end_time - tts_start_time
|
|
255
|
+
await astr_event.send(
|
|
256
|
+
MessageChain(
|
|
257
|
+
type="tts_stats",
|
|
258
|
+
chain=[
|
|
259
|
+
Json(
|
|
260
|
+
data={
|
|
261
|
+
"tts_total_time": tts_duration,
|
|
262
|
+
"tts_first_frame_time": tts_first_frame_time,
|
|
263
|
+
"tts": tts_provider.meta().type,
|
|
264
|
+
"chat_model": agent_runner.provider.get_model(),
|
|
265
|
+
}
|
|
266
|
+
)
|
|
267
|
+
],
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
except Exception as e:
|
|
271
|
+
logger.error(f"发送 TTS 统计信息失败: {e}")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
async def _run_agent_feeder(
|
|
275
|
+
agent_runner: AgentRunner,
|
|
276
|
+
text_queue: asyncio.Queue,
|
|
277
|
+
max_step: int,
|
|
278
|
+
show_tool_use: bool,
|
|
279
|
+
show_reasoning: bool,
|
|
280
|
+
):
|
|
281
|
+
"""运行 Agent 并将文本输出分句放入队列"""
|
|
282
|
+
buffer = ""
|
|
283
|
+
try:
|
|
284
|
+
async for chain in run_agent(
|
|
285
|
+
agent_runner,
|
|
286
|
+
max_step=max_step,
|
|
287
|
+
show_tool_use=show_tool_use,
|
|
288
|
+
stream_to_general=False,
|
|
289
|
+
show_reasoning=show_reasoning,
|
|
290
|
+
):
|
|
291
|
+
if chain is None:
|
|
292
|
+
continue
|
|
293
|
+
|
|
294
|
+
# 提取文本
|
|
295
|
+
text = chain.get_plain_text()
|
|
296
|
+
if text:
|
|
297
|
+
buffer += text
|
|
298
|
+
|
|
299
|
+
# 分句逻辑:匹配标点符号
|
|
300
|
+
# r"([.。!!??\n]+)" 会保留分隔符
|
|
301
|
+
parts = re.split(r"([.。!!??\n]+)", buffer)
|
|
302
|
+
|
|
303
|
+
if len(parts) > 1:
|
|
304
|
+
# 处理完整的句子
|
|
305
|
+
# range step 2 因为 split 后是 [text, delim, text, delim, ...]
|
|
306
|
+
temp_buffer = ""
|
|
307
|
+
for i in range(0, len(parts) - 1, 2):
|
|
308
|
+
sentence = parts[i]
|
|
309
|
+
delim = parts[i + 1]
|
|
310
|
+
full_sentence = sentence + delim
|
|
311
|
+
temp_buffer += full_sentence
|
|
312
|
+
|
|
313
|
+
if len(temp_buffer) >= 10:
|
|
314
|
+
if temp_buffer.strip():
|
|
315
|
+
logger.info(f"[Live Agent Feeder] 分句: {temp_buffer}")
|
|
316
|
+
await text_queue.put(temp_buffer)
|
|
317
|
+
temp_buffer = ""
|
|
318
|
+
|
|
319
|
+
# 更新 buffer 为剩余部分
|
|
320
|
+
buffer = temp_buffer + parts[-1]
|
|
321
|
+
|
|
322
|
+
# 处理剩余 buffer
|
|
323
|
+
if buffer.strip():
|
|
324
|
+
await text_queue.put(buffer)
|
|
325
|
+
|
|
326
|
+
except Exception as e:
|
|
327
|
+
logger.error(f"[Live Agent Feeder] Error: {e}", exc_info=True)
|
|
328
|
+
finally:
|
|
329
|
+
# 发送结束信号
|
|
330
|
+
await text_queue.put(None)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
async def _safe_tts_stream_wrapper(
|
|
334
|
+
tts_provider: TTSProvider,
|
|
335
|
+
text_queue: asyncio.Queue[str | None],
|
|
336
|
+
audio_queue: "asyncio.Queue[bytes | tuple[str, bytes] | None]",
|
|
337
|
+
):
|
|
338
|
+
"""包装原生流式 TTS 确保异常处理和队列关闭"""
|
|
339
|
+
try:
|
|
340
|
+
await tts_provider.get_audio_stream(text_queue, audio_queue)
|
|
341
|
+
except Exception as e:
|
|
342
|
+
logger.error(f"[Live TTS Stream] Error: {e}", exc_info=True)
|
|
343
|
+
finally:
|
|
344
|
+
await audio_queue.put(None)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
async def _simulated_stream_tts(
|
|
348
|
+
tts_provider: TTSProvider,
|
|
349
|
+
text_queue: asyncio.Queue[str | None],
|
|
350
|
+
audio_queue: "asyncio.Queue[bytes | tuple[str, bytes] | None]",
|
|
351
|
+
):
|
|
352
|
+
"""模拟流式 TTS 分句生成音频"""
|
|
353
|
+
try:
|
|
354
|
+
while True:
|
|
355
|
+
text = await text_queue.get()
|
|
356
|
+
if text is None:
|
|
357
|
+
break
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
audio_path = await tts_provider.get_audio(text)
|
|
361
|
+
|
|
362
|
+
if audio_path:
|
|
363
|
+
with open(audio_path, "rb") as f:
|
|
364
|
+
audio_data = f.read()
|
|
365
|
+
await audio_queue.put((text, audio_data))
|
|
366
|
+
except Exception as e:
|
|
367
|
+
logger.error(
|
|
368
|
+
f"[Live TTS Simulated] Error processing text '{text[:20]}...': {e}"
|
|
369
|
+
)
|
|
370
|
+
# 继续处理下一句
|
|
371
|
+
|
|
372
|
+
except Exception as e:
|
|
373
|
+
logger.error(f"[Live TTS Simulated] Critical Error: {e}", exc_info=True)
|
|
374
|
+
finally:
|
|
375
|
+
await audio_queue.put(None)
|
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.12.
|
|
8
|
+
VERSION = "4.12.4"
|
|
9
9
|
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
|
10
10
|
|
|
11
11
|
WEBHOOK_SUPPORTED_PLATFORMS = [
|
|
@@ -321,6 +321,7 @@ CONFIG_METADATA_2 = {
|
|
|
321
321
|
"enable": False,
|
|
322
322
|
"client_id": "",
|
|
323
323
|
"client_secret": "",
|
|
324
|
+
"card_template_id": "",
|
|
324
325
|
},
|
|
325
326
|
"Telegram": {
|
|
326
327
|
"id": "telegram",
|
|
@@ -582,6 +583,11 @@ CONFIG_METADATA_2 = {
|
|
|
582
583
|
"type": "string",
|
|
583
584
|
"hint": "可选:填写 Misskey 网盘中目标文件夹的 ID,上传的文件将放置到该文件夹内。留空则使用账号网盘根目录。",
|
|
584
585
|
},
|
|
586
|
+
"card_template_id": {
|
|
587
|
+
"description": "卡片模板 ID",
|
|
588
|
+
"type": "string",
|
|
589
|
+
"hint": "可选。钉钉互动卡片模板 ID。启用后将使用互动卡片进行流式回复。",
|
|
590
|
+
},
|
|
585
591
|
"telegram_command_register": {
|
|
586
592
|
"description": "Telegram 命令注册",
|
|
587
593
|
"type": "bool",
|
|
@@ -1179,6 +1185,19 @@ CONFIG_METADATA_2 = {
|
|
|
1179
1185
|
"openai-tts-voice": "alloy",
|
|
1180
1186
|
"timeout": "20",
|
|
1181
1187
|
},
|
|
1188
|
+
"Genie TTS": {
|
|
1189
|
+
"id": "genie_tts",
|
|
1190
|
+
"provider": "genie_tts",
|
|
1191
|
+
"type": "genie_tts",
|
|
1192
|
+
"provider_type": "text_to_speech",
|
|
1193
|
+
"enable": False,
|
|
1194
|
+
"genie_character_name": "mika",
|
|
1195
|
+
"genie_onnx_model_dir": "CharacterModels/v2ProPlus/mika/tts_models",
|
|
1196
|
+
"genie_language": "Japanese",
|
|
1197
|
+
"genie_refer_audio_path": "",
|
|
1198
|
+
"genie_refer_text": "",
|
|
1199
|
+
"timeout": 20,
|
|
1200
|
+
},
|
|
1182
1201
|
"Edge TTS": {
|
|
1183
1202
|
"id": "edge_tts",
|
|
1184
1203
|
"provider": "microsoft",
|
|
@@ -1395,6 +1414,16 @@ CONFIG_METADATA_2 = {
|
|
|
1395
1414
|
},
|
|
1396
1415
|
},
|
|
1397
1416
|
"items": {
|
|
1417
|
+
"genie_onnx_model_dir": {
|
|
1418
|
+
"description": "ONNX Model Directory",
|
|
1419
|
+
"type": "string",
|
|
1420
|
+
"hint": "The directory path containing the ONNX model files",
|
|
1421
|
+
},
|
|
1422
|
+
"genie_language": {
|
|
1423
|
+
"description": "Language",
|
|
1424
|
+
"type": "string",
|
|
1425
|
+
"options": ["Japanese", "English", "Chinese"],
|
|
1426
|
+
},
|
|
1398
1427
|
"provider_source_id": {
|
|
1399
1428
|
"invisible": True,
|
|
1400
1429
|
"type": "string",
|
astrbot/core/db/__init__.py
CHANGED
|
@@ -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,19 @@ 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
|
+
folder_id: str | None = None,
|
|
258
|
+
sort_order: int = 0,
|
|
256
259
|
) -> Persona:
|
|
257
|
-
"""Insert a new persona record.
|
|
260
|
+
"""Insert a new persona record.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
persona_id: Unique identifier for the persona
|
|
264
|
+
system_prompt: System prompt for the persona
|
|
265
|
+
begin_dialogs: Optional list of initial dialog strings
|
|
266
|
+
tools: Optional list of tool names (None means all tools, [] means no tools)
|
|
267
|
+
folder_id: Optional folder ID to place the persona in (None means root)
|
|
268
|
+
sort_order: Sort order within the folder (default 0)
|
|
269
|
+
"""
|
|
258
270
|
...
|
|
259
271
|
|
|
260
272
|
@abc.abstractmethod
|
|
@@ -283,6 +295,84 @@ class BaseDatabase(abc.ABC):
|
|
|
283
295
|
"""Delete a persona by its ID."""
|
|
284
296
|
...
|
|
285
297
|
|
|
298
|
+
# ====
|
|
299
|
+
# Persona Folder Management
|
|
300
|
+
# ====
|
|
301
|
+
|
|
302
|
+
@abc.abstractmethod
|
|
303
|
+
async def insert_persona_folder(
|
|
304
|
+
self,
|
|
305
|
+
name: str,
|
|
306
|
+
parent_id: str | None = None,
|
|
307
|
+
description: str | None = None,
|
|
308
|
+
sort_order: int = 0,
|
|
309
|
+
) -> PersonaFolder:
|
|
310
|
+
"""Insert a new persona folder."""
|
|
311
|
+
...
|
|
312
|
+
|
|
313
|
+
@abc.abstractmethod
|
|
314
|
+
async def get_persona_folder_by_id(self, folder_id: str) -> PersonaFolder | None:
|
|
315
|
+
"""Get a persona folder by its folder_id."""
|
|
316
|
+
...
|
|
317
|
+
|
|
318
|
+
@abc.abstractmethod
|
|
319
|
+
async def get_persona_folders(
|
|
320
|
+
self, parent_id: str | None = None
|
|
321
|
+
) -> list[PersonaFolder]:
|
|
322
|
+
"""Get all persona folders, optionally filtered by parent_id."""
|
|
323
|
+
...
|
|
324
|
+
|
|
325
|
+
@abc.abstractmethod
|
|
326
|
+
async def get_all_persona_folders(self) -> list[PersonaFolder]:
|
|
327
|
+
"""Get all persona folders."""
|
|
328
|
+
...
|
|
329
|
+
|
|
330
|
+
@abc.abstractmethod
|
|
331
|
+
async def update_persona_folder(
|
|
332
|
+
self,
|
|
333
|
+
folder_id: str,
|
|
334
|
+
name: str | None = None,
|
|
335
|
+
parent_id: T.Any = None,
|
|
336
|
+
description: T.Any = None,
|
|
337
|
+
sort_order: int | None = None,
|
|
338
|
+
) -> PersonaFolder | None:
|
|
339
|
+
"""Update a persona folder."""
|
|
340
|
+
...
|
|
341
|
+
|
|
342
|
+
@abc.abstractmethod
|
|
343
|
+
async def delete_persona_folder(self, folder_id: str) -> None:
|
|
344
|
+
"""Delete a persona folder by its folder_id."""
|
|
345
|
+
...
|
|
346
|
+
|
|
347
|
+
@abc.abstractmethod
|
|
348
|
+
async def move_persona_to_folder(
|
|
349
|
+
self, persona_id: str, folder_id: str | None
|
|
350
|
+
) -> Persona | None:
|
|
351
|
+
"""Move a persona to a folder (or root if folder_id is None)."""
|
|
352
|
+
...
|
|
353
|
+
|
|
354
|
+
@abc.abstractmethod
|
|
355
|
+
async def get_personas_by_folder(
|
|
356
|
+
self, folder_id: str | None = None
|
|
357
|
+
) -> list[Persona]:
|
|
358
|
+
"""Get all personas in a specific folder."""
|
|
359
|
+
...
|
|
360
|
+
|
|
361
|
+
@abc.abstractmethod
|
|
362
|
+
async def batch_update_sort_order(
|
|
363
|
+
self,
|
|
364
|
+
items: list[dict],
|
|
365
|
+
) -> None:
|
|
366
|
+
"""Batch update sort_order for personas and/or folders.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
items: List of dicts with keys:
|
|
370
|
+
- id: The persona_id or folder_id
|
|
371
|
+
- type: Either "persona" or "folder"
|
|
372
|
+
- sort_order: The new sort_order value
|
|
373
|
+
"""
|
|
374
|
+
...
|
|
375
|
+
|
|
286
376
|
@abc.abstractmethod
|
|
287
377
|
async def insert_preference_or_update(
|
|
288
378
|
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,10 @@ 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
|
+
folder_id: str | None = Field(default=None, max_length=36)
|
|
129
|
+
"""所属文件夹ID,NULL 表示在根目录"""
|
|
130
|
+
sort_order: int = Field(default=0)
|
|
131
|
+
"""排序顺序"""
|
|
90
132
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
91
133
|
updated_at: datetime = Field(
|
|
92
134
|
default_factory=lambda: datetime.now(timezone.utc),
|