AstrBot 4.9.1__py3-none-any.whl → 4.10.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/cli/__init__.py +1 -1
- astrbot/core/agent/message.py +6 -4
- astrbot/core/agent/response.py +22 -1
- astrbot/core/agent/run_context.py +1 -1
- astrbot/core/agent/runners/tool_loop_agent_runner.py +99 -20
- astrbot/core/astr_agent_context.py +3 -1
- astrbot/core/astr_agent_run_util.py +42 -3
- astrbot/core/astr_agent_tool_exec.py +34 -4
- astrbot/core/config/default.py +127 -184
- astrbot/core/core_lifecycle.py +3 -0
- astrbot/core/db/__init__.py +72 -0
- astrbot/core/db/po.py +59 -0
- astrbot/core/db/sqlite.py +240 -0
- astrbot/core/message/components.py +4 -5
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +6 -1
- astrbot/core/pipeline/respond/stage.py +1 -1
- astrbot/core/platform/sources/telegram/tg_event.py +9 -0
- astrbot/core/platform/sources/webchat/webchat_event.py +22 -18
- astrbot/core/provider/entities.py +41 -0
- astrbot/core/provider/manager.py +203 -93
- astrbot/core/provider/sources/anthropic_source.py +55 -11
- astrbot/core/provider/sources/gemini_source.py +84 -33
- astrbot/core/provider/sources/openai_source.py +21 -6
- astrbot/core/star/__init__.py +5 -1
- astrbot/core/star/command_management.py +449 -0
- astrbot/core/star/context.py +4 -0
- astrbot/core/star/filter/command.py +1 -0
- astrbot/core/star/filter/command_group.py +1 -0
- astrbot/core/star/star_handler.py +4 -0
- astrbot/core/star/star_manager.py +14 -0
- astrbot/core/utils/llm_metadata.py +63 -0
- astrbot/core/utils/migra_helper.py +93 -0
- astrbot/core/utils/plugin_kv_store.py +28 -0
- astrbot/dashboard/routes/__init__.py +2 -0
- astrbot/dashboard/routes/chat.py +56 -13
- astrbot/dashboard/routes/command.py +82 -0
- astrbot/dashboard/routes/config.py +291 -33
- astrbot/dashboard/routes/stat.py +96 -0
- astrbot/dashboard/routes/tools.py +20 -4
- astrbot/dashboard/server.py +1 -0
- {astrbot-4.9.1.dist-info → astrbot-4.10.0.dist-info}/METADATA +2 -2
- {astrbot-4.9.1.dist-info → astrbot-4.10.0.dist-info}/RECORD +45 -41
- {astrbot-4.9.1.dist-info → astrbot-4.10.0.dist-info}/WHEEL +0 -0
- {astrbot-4.9.1.dist-info → astrbot-4.10.0.dist-info}/entry_points.txt +0 -0
- {astrbot-4.9.1.dist-info → astrbot-4.10.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -32,6 +32,92 @@ def _migra_agent_runner_configs(conf: AstrBotConfig, ids_map: dict) -> None:
|
|
|
32
32
|
logger.error(traceback.format_exc())
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
def _migra_provider_to_source_structure(conf: AstrBotConfig) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Migrate old provider structure to new provider-source separation.
|
|
38
|
+
Provider only keeps: id, provider_source_id, model, modalities, custom_extra_body
|
|
39
|
+
All other fields move to provider_sources.
|
|
40
|
+
"""
|
|
41
|
+
providers = conf.get("provider", [])
|
|
42
|
+
provider_sources = conf.get("provider_sources", [])
|
|
43
|
+
|
|
44
|
+
# Track if any migration happened
|
|
45
|
+
migrated = False
|
|
46
|
+
|
|
47
|
+
# Provider-only fields that should stay in provider
|
|
48
|
+
provider_only_fields = {
|
|
49
|
+
"id",
|
|
50
|
+
"provider_source_id",
|
|
51
|
+
"model",
|
|
52
|
+
"modalities",
|
|
53
|
+
"custom_extra_body",
|
|
54
|
+
"enable",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Fields that should not go to source
|
|
58
|
+
source_exclude_fields = provider_only_fields | {"model_config"}
|
|
59
|
+
|
|
60
|
+
for provider in providers:
|
|
61
|
+
# Skip if already has provider_source_id
|
|
62
|
+
if provider.get("provider_source_id"):
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
# Skip non-chat-completion types (they don't need source separation)
|
|
66
|
+
provider_type = provider.get("provider_type", "")
|
|
67
|
+
if provider_type != "chat_completion":
|
|
68
|
+
# For old types without provider_type, check type field
|
|
69
|
+
old_type = provider.get("type", "")
|
|
70
|
+
if "chat_completion" not in old_type:
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
migrated = True
|
|
74
|
+
logger.info(f"Migrating provider {provider.get('id')} to new structure")
|
|
75
|
+
|
|
76
|
+
# Extract source fields from provider
|
|
77
|
+
source_fields = {}
|
|
78
|
+
for key, value in list(provider.items()):
|
|
79
|
+
if key not in source_exclude_fields:
|
|
80
|
+
source_fields[key] = value
|
|
81
|
+
|
|
82
|
+
# Create new provider_source
|
|
83
|
+
source_id = provider.get("id", "") + "_source"
|
|
84
|
+
new_source = {"id": source_id, **source_fields}
|
|
85
|
+
|
|
86
|
+
# Update provider to only keep necessary fields
|
|
87
|
+
provider["provider_source_id"] = source_id
|
|
88
|
+
|
|
89
|
+
# Extract model from model_config if exists
|
|
90
|
+
if "model_config" in provider and isinstance(provider["model_config"], dict):
|
|
91
|
+
model_config = provider["model_config"]
|
|
92
|
+
provider["model"] = model_config.get("model", "")
|
|
93
|
+
|
|
94
|
+
# Put other model_config fields into custom_extra_body
|
|
95
|
+
extra_body_fields = {k: v for k, v in model_config.items() if k != "model"}
|
|
96
|
+
if extra_body_fields:
|
|
97
|
+
if "custom_extra_body" not in provider:
|
|
98
|
+
provider["custom_extra_body"] = {}
|
|
99
|
+
provider["custom_extra_body"].update(extra_body_fields)
|
|
100
|
+
|
|
101
|
+
# Initialize new fields if not present
|
|
102
|
+
if "modalities" not in provider:
|
|
103
|
+
provider["modalities"] = []
|
|
104
|
+
if "custom_extra_body" not in provider:
|
|
105
|
+
provider["custom_extra_body"] = {}
|
|
106
|
+
|
|
107
|
+
# Remove fields that should be in source
|
|
108
|
+
keys_to_remove = [k for k in provider.keys() if k not in provider_only_fields]
|
|
109
|
+
for key in keys_to_remove:
|
|
110
|
+
del provider[key]
|
|
111
|
+
|
|
112
|
+
# Add source to provider_sources
|
|
113
|
+
provider_sources.append(new_source)
|
|
114
|
+
|
|
115
|
+
if migrated:
|
|
116
|
+
conf["provider_sources"] = provider_sources
|
|
117
|
+
conf.save_config()
|
|
118
|
+
logger.info("Provider-source structure migration completed")
|
|
119
|
+
|
|
120
|
+
|
|
35
121
|
async def migra(
|
|
36
122
|
db, astrbot_config_mgr, umop_config_router, acm: AstrBotConfigManager
|
|
37
123
|
) -> None:
|
|
@@ -71,3 +157,10 @@ async def migra(
|
|
|
71
157
|
|
|
72
158
|
for conf in acm.confs.values():
|
|
73
159
|
_migra_agent_runner_configs(conf, ids_map)
|
|
160
|
+
|
|
161
|
+
# Migrate providers to new structure: extract source fields to provider_sources
|
|
162
|
+
try:
|
|
163
|
+
_migra_provider_to_source_structure(astrbot_config)
|
|
164
|
+
except Exception as e:
|
|
165
|
+
logger.error(f"Migration for provider-source structure failed: {e!s}")
|
|
166
|
+
logger.error(traceback.format_exc())
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from typing import TypeVar
|
|
2
|
+
|
|
3
|
+
from astrbot.core import sp
|
|
4
|
+
|
|
5
|
+
SUPPORTED_VALUE_TYPES = int | float | str | bytes | bool | dict | list | None
|
|
6
|
+
_VT = TypeVar("_VT")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PluginKVStoreMixin:
|
|
10
|
+
"""为插件提供键值存储功能的 Mixin 类"""
|
|
11
|
+
|
|
12
|
+
plugin_id: str
|
|
13
|
+
|
|
14
|
+
async def put_kv_data(
|
|
15
|
+
self,
|
|
16
|
+
key: str,
|
|
17
|
+
value: SUPPORTED_VALUE_TYPES,
|
|
18
|
+
) -> None:
|
|
19
|
+
"""为指定插件存储一个键值对"""
|
|
20
|
+
await sp.put_async("plugin", self.plugin_id, key, value)
|
|
21
|
+
|
|
22
|
+
async def get_kv_data(self, key: str, default: _VT) -> _VT | None:
|
|
23
|
+
"""获取指定插件存储的键值对"""
|
|
24
|
+
return await sp.get_async("plugin", self.plugin_id, key, default)
|
|
25
|
+
|
|
26
|
+
async def delete_kv_data(self, key: str) -> None:
|
|
27
|
+
"""删除指定插件存储的键值对"""
|
|
28
|
+
await sp.remove_async("plugin", self.plugin_id, key)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from .auth import AuthRoute
|
|
2
2
|
from .chat import ChatRoute
|
|
3
|
+
from .command import CommandRoute
|
|
3
4
|
from .config import ConfigRoute
|
|
4
5
|
from .conversation import ConversationRoute
|
|
5
6
|
from .file import FileRoute
|
|
@@ -17,6 +18,7 @@ from .update import UpdateRoute
|
|
|
17
18
|
__all__ = [
|
|
18
19
|
"AuthRoute",
|
|
19
20
|
"ChatRoute",
|
|
21
|
+
"CommandRoute",
|
|
20
22
|
"ConfigRoute",
|
|
21
23
|
"ConversationRoute",
|
|
22
24
|
"FileRoute",
|
astrbot/dashboard/routes/chat.py
CHANGED
|
@@ -227,16 +227,19 @@ class ChatRoute(Route):
|
|
|
227
227
|
text: str,
|
|
228
228
|
media_parts: list,
|
|
229
229
|
reasoning: str,
|
|
230
|
+
agent_stats: dict,
|
|
230
231
|
):
|
|
231
232
|
"""保存 bot 消息到历史记录,返回保存的记录"""
|
|
232
233
|
bot_message_parts = []
|
|
234
|
+
bot_message_parts.extend(media_parts)
|
|
233
235
|
if text:
|
|
234
236
|
bot_message_parts.append({"type": "plain", "text": text})
|
|
235
|
-
bot_message_parts.extend(media_parts)
|
|
236
237
|
|
|
237
238
|
new_his = {"type": "bot", "message": bot_message_parts}
|
|
238
239
|
if reasoning:
|
|
239
240
|
new_his["reasoning"] = reasoning
|
|
241
|
+
if agent_stats:
|
|
242
|
+
new_his["agent_stats"] = agent_stats
|
|
240
243
|
|
|
241
244
|
record = await self.platform_history_mgr.insert(
|
|
242
245
|
platform_id="webchat",
|
|
@@ -294,7 +297,8 @@ class ChatRoute(Route):
|
|
|
294
297
|
accumulated_parts = []
|
|
295
298
|
accumulated_text = ""
|
|
296
299
|
accumulated_reasoning = ""
|
|
297
|
-
|
|
300
|
+
tool_calls = {}
|
|
301
|
+
agent_stats = {}
|
|
298
302
|
try:
|
|
299
303
|
async with track_conversation(self.running_convs, webchat_conv_id):
|
|
300
304
|
while True:
|
|
@@ -314,6 +318,16 @@ class ChatRoute(Route):
|
|
|
314
318
|
result_text = result["data"]
|
|
315
319
|
msg_type = result.get("type")
|
|
316
320
|
streaming = result.get("streaming", False)
|
|
321
|
+
chain_type = result.get("chain_type")
|
|
322
|
+
|
|
323
|
+
if chain_type == "agent_stats":
|
|
324
|
+
stats_info = {
|
|
325
|
+
"type": "agent_stats",
|
|
326
|
+
"data": json.loads(result_text),
|
|
327
|
+
}
|
|
328
|
+
yield f"data: {json.dumps(stats_info, ensure_ascii=False)}\n\n"
|
|
329
|
+
agent_stats = stats_info["data"]
|
|
330
|
+
continue
|
|
317
331
|
|
|
318
332
|
# 发送 SSE 数据
|
|
319
333
|
try:
|
|
@@ -335,11 +349,35 @@ class ChatRoute(Route):
|
|
|
335
349
|
|
|
336
350
|
# 累积消息部分
|
|
337
351
|
if msg_type == "plain":
|
|
338
|
-
chain_type = result.get("chain_type"
|
|
339
|
-
if chain_type == "
|
|
352
|
+
chain_type = result.get("chain_type")
|
|
353
|
+
if chain_type == "tool_call":
|
|
354
|
+
tool_call = json.loads(result_text)
|
|
355
|
+
tool_calls[tool_call.get("id")] = tool_call
|
|
356
|
+
if accumulated_text:
|
|
357
|
+
# 如果累积了文本,则先保存文本
|
|
358
|
+
accumulated_parts.append(
|
|
359
|
+
{"type": "plain", "text": accumulated_text}
|
|
360
|
+
)
|
|
361
|
+
accumulated_text = ""
|
|
362
|
+
elif chain_type == "tool_call_result":
|
|
363
|
+
tcr = json.loads(result_text)
|
|
364
|
+
tc_id = tcr.get("id")
|
|
365
|
+
if tc_id in tool_calls:
|
|
366
|
+
tool_calls[tc_id]["result"] = tcr.get("result")
|
|
367
|
+
tool_calls[tc_id]["finished_ts"] = tcr.get("ts")
|
|
368
|
+
accumulated_parts.append(
|
|
369
|
+
{
|
|
370
|
+
"type": "tool_call",
|
|
371
|
+
"tool_calls": [tool_calls[tc_id]],
|
|
372
|
+
}
|
|
373
|
+
)
|
|
374
|
+
tool_calls.pop(tc_id, None)
|
|
375
|
+
elif chain_type == "reasoning":
|
|
340
376
|
accumulated_reasoning += result_text
|
|
341
|
-
|
|
377
|
+
elif streaming:
|
|
342
378
|
accumulated_text += result_text
|
|
379
|
+
else:
|
|
380
|
+
accumulated_text = result_text
|
|
343
381
|
elif msg_type == "image":
|
|
344
382
|
filename = result_text.replace("[IMAGE]", "")
|
|
345
383
|
part = await self._create_attachment_from_file(
|
|
@@ -367,15 +405,20 @@ class ChatRoute(Route):
|
|
|
367
405
|
if msg_type == "end":
|
|
368
406
|
break
|
|
369
407
|
elif (
|
|
370
|
-
(streaming and msg_type == "complete")
|
|
371
|
-
or
|
|
372
|
-
or msg_type == "break"
|
|
408
|
+
(streaming and msg_type == "complete") or not streaming
|
|
409
|
+
# or msg_type == "break"
|
|
373
410
|
):
|
|
411
|
+
if (
|
|
412
|
+
chain_type == "tool_call"
|
|
413
|
+
or chain_type == "tool_call_result"
|
|
414
|
+
):
|
|
415
|
+
continue
|
|
374
416
|
saved_record = await self._save_bot_message(
|
|
375
417
|
webchat_conv_id,
|
|
376
418
|
accumulated_text,
|
|
377
419
|
accumulated_parts,
|
|
378
420
|
accumulated_reasoning,
|
|
421
|
+
agent_stats,
|
|
379
422
|
)
|
|
380
423
|
# 发送保存的消息信息给前端
|
|
381
424
|
if saved_record and not client_disconnected:
|
|
@@ -390,11 +433,11 @@ class ChatRoute(Route):
|
|
|
390
433
|
yield f"data: {json.dumps(saved_info, ensure_ascii=False)}\n\n"
|
|
391
434
|
except Exception:
|
|
392
435
|
pass
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
436
|
+
accumulated_parts = []
|
|
437
|
+
accumulated_text = ""
|
|
438
|
+
accumulated_reasoning = ""
|
|
439
|
+
# tool_calls = {}
|
|
440
|
+
agent_stats = {}
|
|
398
441
|
except BaseException as e:
|
|
399
442
|
logger.exception(f"WebChat stream unexpected error: {e}", exc_info=True)
|
|
400
443
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from quart import request
|
|
2
|
+
|
|
3
|
+
from astrbot.core.star.command_management import (
|
|
4
|
+
list_command_conflicts,
|
|
5
|
+
list_commands,
|
|
6
|
+
)
|
|
7
|
+
from astrbot.core.star.command_management import (
|
|
8
|
+
rename_command as rename_command_service,
|
|
9
|
+
)
|
|
10
|
+
from astrbot.core.star.command_management import (
|
|
11
|
+
toggle_command as toggle_command_service,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from .route import Response, Route, RouteContext
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CommandRoute(Route):
|
|
18
|
+
def __init__(self, context: RouteContext) -> None:
|
|
19
|
+
super().__init__(context)
|
|
20
|
+
self.routes = {
|
|
21
|
+
"/commands": ("GET", self.get_commands),
|
|
22
|
+
"/commands/conflicts": ("GET", self.get_conflicts),
|
|
23
|
+
"/commands/toggle": ("POST", self.toggle_command),
|
|
24
|
+
"/commands/rename": ("POST", self.rename_command),
|
|
25
|
+
}
|
|
26
|
+
self.register_routes()
|
|
27
|
+
|
|
28
|
+
async def get_commands(self):
|
|
29
|
+
commands = await list_commands()
|
|
30
|
+
summary = {
|
|
31
|
+
"total": len(commands),
|
|
32
|
+
"disabled": len([cmd for cmd in commands if not cmd["enabled"]]),
|
|
33
|
+
"conflicts": len([cmd for cmd in commands if cmd.get("has_conflict")]),
|
|
34
|
+
}
|
|
35
|
+
return Response().ok({"items": commands, "summary": summary}).__dict__
|
|
36
|
+
|
|
37
|
+
async def get_conflicts(self):
|
|
38
|
+
conflicts = await list_command_conflicts()
|
|
39
|
+
return Response().ok(conflicts).__dict__
|
|
40
|
+
|
|
41
|
+
async def toggle_command(self):
|
|
42
|
+
data = await request.get_json()
|
|
43
|
+
handler_full_name = data.get("handler_full_name")
|
|
44
|
+
enabled = data.get("enabled")
|
|
45
|
+
|
|
46
|
+
if handler_full_name is None or enabled is None:
|
|
47
|
+
return Response().error("handler_full_name 与 enabled 均为必填。").__dict__
|
|
48
|
+
|
|
49
|
+
if isinstance(enabled, str):
|
|
50
|
+
enabled = enabled.lower() in ("1", "true", "yes", "on")
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
await toggle_command_service(handler_full_name, bool(enabled))
|
|
54
|
+
except ValueError as exc:
|
|
55
|
+
return Response().error(str(exc)).__dict__
|
|
56
|
+
|
|
57
|
+
payload = await _get_command_payload(handler_full_name)
|
|
58
|
+
return Response().ok(payload).__dict__
|
|
59
|
+
|
|
60
|
+
async def rename_command(self):
|
|
61
|
+
data = await request.get_json()
|
|
62
|
+
handler_full_name = data.get("handler_full_name")
|
|
63
|
+
new_name = data.get("new_name")
|
|
64
|
+
|
|
65
|
+
if not handler_full_name or not new_name:
|
|
66
|
+
return Response().error("handler_full_name 与 new_name 均为必填。").__dict__
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
await rename_command_service(handler_full_name, new_name)
|
|
70
|
+
except ValueError as exc:
|
|
71
|
+
return Response().error(str(exc)).__dict__
|
|
72
|
+
|
|
73
|
+
payload = await _get_command_payload(handler_full_name)
|
|
74
|
+
return Response().ok(payload).__dict__
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def _get_command_payload(handler_full_name: str):
|
|
78
|
+
commands = await list_commands()
|
|
79
|
+
for cmd in commands:
|
|
80
|
+
if cmd["handler_full_name"] == handler_full_name:
|
|
81
|
+
return cmd
|
|
82
|
+
return {}
|