AstrBot 4.9.2__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/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 +2 -0
- astrbot/core/utils/llm_metadata.py +63 -0
- astrbot/core/utils/migra_helper.py +93 -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.2.dist-info → astrbot-4.10.0.dist-info}/METADATA +2 -2
- {astrbot-4.9.2.dist-info → astrbot-4.10.0.dist-info}/RECORD +43 -40
- {astrbot-4.9.2.dist-info → astrbot-4.10.0.dist-info}/WHEEL +0 -0
- {astrbot-4.9.2.dist-info → astrbot-4.10.0.dist-info}/entry_points.txt +0 -0
- {astrbot-4.9.2.dist-info → astrbot-4.10.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from astrbot.core import db_helper
|
|
8
|
+
from astrbot.core.db.po import CommandConfig
|
|
9
|
+
from astrbot.core.star.filter.command import CommandFilter
|
|
10
|
+
from astrbot.core.star.filter.command_group import CommandGroupFilter
|
|
11
|
+
from astrbot.core.star.filter.permission import PermissionType, PermissionTypeFilter
|
|
12
|
+
from astrbot.core.star.star import star_map
|
|
13
|
+
from astrbot.core.star.star_handler import StarHandlerMetadata, star_handlers_registry
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class CommandDescriptor:
|
|
18
|
+
handler: StarHandlerMetadata = field(repr=False)
|
|
19
|
+
filter_ref: CommandFilter | CommandGroupFilter | None = field(
|
|
20
|
+
default=None,
|
|
21
|
+
repr=False,
|
|
22
|
+
)
|
|
23
|
+
handler_full_name: str = ""
|
|
24
|
+
handler_name: str = ""
|
|
25
|
+
plugin_name: str = ""
|
|
26
|
+
plugin_display_name: str | None = None
|
|
27
|
+
module_path: str = ""
|
|
28
|
+
description: str = ""
|
|
29
|
+
command_type: str = "command" # "command" | "group" | "sub_command"
|
|
30
|
+
raw_command_name: str | None = None
|
|
31
|
+
current_fragment: str | None = None
|
|
32
|
+
parent_signature: str = ""
|
|
33
|
+
parent_group_handler: str = ""
|
|
34
|
+
original_command: str | None = None
|
|
35
|
+
effective_command: str | None = None
|
|
36
|
+
aliases: list[str] = field(default_factory=list)
|
|
37
|
+
permission: str = "everyone"
|
|
38
|
+
enabled: bool = True
|
|
39
|
+
is_group: bool = False
|
|
40
|
+
is_sub_command: bool = False
|
|
41
|
+
reserved: bool = False
|
|
42
|
+
config: CommandConfig | None = None
|
|
43
|
+
has_conflict: bool = False
|
|
44
|
+
sub_commands: list[CommandDescriptor] = field(default_factory=list)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def sync_command_configs() -> None:
|
|
48
|
+
"""同步指令配置,清理过期配置。"""
|
|
49
|
+
descriptors = _collect_descriptors(include_sub_commands=False)
|
|
50
|
+
config_records = await db_helper.get_command_configs()
|
|
51
|
+
config_map = _bind_configs_to_descriptors(descriptors, config_records)
|
|
52
|
+
live_handlers = {desc.handler_full_name for desc in descriptors}
|
|
53
|
+
|
|
54
|
+
stale_configs = [key for key in config_map if key not in live_handlers]
|
|
55
|
+
if stale_configs:
|
|
56
|
+
await db_helper.delete_command_configs(stale_configs)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def toggle_command(handler_full_name: str, enabled: bool) -> CommandDescriptor:
|
|
60
|
+
descriptor = _build_descriptor_by_full_name(handler_full_name)
|
|
61
|
+
if not descriptor:
|
|
62
|
+
raise ValueError("指定的处理函数不存在或不是指令。")
|
|
63
|
+
|
|
64
|
+
existing_cfg = await db_helper.get_command_config(handler_full_name)
|
|
65
|
+
config = await db_helper.upsert_command_config(
|
|
66
|
+
handler_full_name=handler_full_name,
|
|
67
|
+
plugin_name=descriptor.plugin_name or "",
|
|
68
|
+
module_path=descriptor.module_path,
|
|
69
|
+
original_command=descriptor.original_command or descriptor.handler_name,
|
|
70
|
+
resolved_command=(
|
|
71
|
+
existing_cfg.resolved_command
|
|
72
|
+
if existing_cfg
|
|
73
|
+
else descriptor.current_fragment
|
|
74
|
+
),
|
|
75
|
+
enabled=enabled,
|
|
76
|
+
keep_original_alias=False,
|
|
77
|
+
conflict_key=existing_cfg.conflict_key
|
|
78
|
+
if existing_cfg and existing_cfg.conflict_key
|
|
79
|
+
else descriptor.original_command,
|
|
80
|
+
resolution_strategy=existing_cfg.resolution_strategy if existing_cfg else None,
|
|
81
|
+
note=existing_cfg.note if existing_cfg else None,
|
|
82
|
+
extra_data=existing_cfg.extra_data if existing_cfg else None,
|
|
83
|
+
auto_managed=False,
|
|
84
|
+
)
|
|
85
|
+
_bind_descriptor_with_config(descriptor, config)
|
|
86
|
+
await sync_command_configs()
|
|
87
|
+
return descriptor
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async def rename_command(
|
|
91
|
+
handler_full_name: str,
|
|
92
|
+
new_fragment: str,
|
|
93
|
+
) -> CommandDescriptor:
|
|
94
|
+
descriptor = _build_descriptor_by_full_name(handler_full_name)
|
|
95
|
+
if not descriptor:
|
|
96
|
+
raise ValueError("指定的处理函数不存在或不是指令。")
|
|
97
|
+
|
|
98
|
+
new_fragment = new_fragment.strip()
|
|
99
|
+
if not new_fragment:
|
|
100
|
+
raise ValueError("指令名不能为空。")
|
|
101
|
+
|
|
102
|
+
candidate_full = _compose_command(descriptor.parent_signature, new_fragment)
|
|
103
|
+
if _is_command_in_use(handler_full_name, candidate_full):
|
|
104
|
+
raise ValueError("新的指令名已被其他指令占用,请换一个名称。")
|
|
105
|
+
|
|
106
|
+
config = await db_helper.upsert_command_config(
|
|
107
|
+
handler_full_name=handler_full_name,
|
|
108
|
+
plugin_name=descriptor.plugin_name or "",
|
|
109
|
+
module_path=descriptor.module_path,
|
|
110
|
+
original_command=descriptor.original_command or descriptor.handler_name,
|
|
111
|
+
resolved_command=new_fragment,
|
|
112
|
+
enabled=True if descriptor.enabled else False,
|
|
113
|
+
keep_original_alias=False,
|
|
114
|
+
conflict_key=descriptor.original_command,
|
|
115
|
+
resolution_strategy="manual_rename",
|
|
116
|
+
note=None,
|
|
117
|
+
extra_data=None,
|
|
118
|
+
auto_managed=False,
|
|
119
|
+
)
|
|
120
|
+
_bind_descriptor_with_config(descriptor, config)
|
|
121
|
+
|
|
122
|
+
await sync_command_configs()
|
|
123
|
+
return descriptor
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def list_commands() -> list[dict[str, Any]]:
|
|
127
|
+
descriptors = _collect_descriptors(include_sub_commands=True)
|
|
128
|
+
config_records = await db_helper.get_command_configs()
|
|
129
|
+
_bind_configs_to_descriptors(descriptors, config_records)
|
|
130
|
+
|
|
131
|
+
conflict_groups = _group_conflicts(descriptors)
|
|
132
|
+
conflict_handler_names: set[str] = {
|
|
133
|
+
d.handler_full_name for group in conflict_groups.values() for d in group
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# 分类,设置冲突标志,将子指令挂载到父指令组
|
|
137
|
+
group_map: dict[str, CommandDescriptor] = {}
|
|
138
|
+
sub_commands: list[CommandDescriptor] = []
|
|
139
|
+
root_commands: list[CommandDescriptor] = []
|
|
140
|
+
|
|
141
|
+
for desc in descriptors:
|
|
142
|
+
desc.has_conflict = desc.handler_full_name in conflict_handler_names
|
|
143
|
+
if desc.is_group:
|
|
144
|
+
group_map[desc.handler_full_name] = desc
|
|
145
|
+
elif desc.is_sub_command:
|
|
146
|
+
sub_commands.append(desc)
|
|
147
|
+
else:
|
|
148
|
+
root_commands.append(desc)
|
|
149
|
+
|
|
150
|
+
for sub in sub_commands:
|
|
151
|
+
if sub.parent_group_handler and sub.parent_group_handler in group_map:
|
|
152
|
+
group_map[sub.parent_group_handler].sub_commands.append(sub)
|
|
153
|
+
else:
|
|
154
|
+
root_commands.append(sub)
|
|
155
|
+
|
|
156
|
+
# 指令组 + 普通指令,按 effective_command 字母排序
|
|
157
|
+
all_commands = list(group_map.values()) + root_commands
|
|
158
|
+
all_commands.sort(key=lambda d: (d.effective_command or "").lower())
|
|
159
|
+
|
|
160
|
+
result = [_descriptor_to_dict(desc) for desc in all_commands]
|
|
161
|
+
return result
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
async def list_command_conflicts() -> list[dict[str, Any]]:
|
|
165
|
+
"""列出所有冲突的指令组。"""
|
|
166
|
+
descriptors = _collect_descriptors(include_sub_commands=False)
|
|
167
|
+
config_records = await db_helper.get_command_configs()
|
|
168
|
+
_bind_configs_to_descriptors(descriptors, config_records)
|
|
169
|
+
|
|
170
|
+
conflict_groups = _group_conflicts(descriptors)
|
|
171
|
+
details = [
|
|
172
|
+
{
|
|
173
|
+
"conflict_key": key,
|
|
174
|
+
"handlers": [
|
|
175
|
+
{
|
|
176
|
+
"handler_full_name": item.handler_full_name,
|
|
177
|
+
"plugin": item.plugin_name,
|
|
178
|
+
"current_name": item.effective_command,
|
|
179
|
+
}
|
|
180
|
+
for item in group
|
|
181
|
+
],
|
|
182
|
+
}
|
|
183
|
+
for key, group in conflict_groups.items()
|
|
184
|
+
]
|
|
185
|
+
return details
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# Internal helpers ----------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _collect_descriptors(include_sub_commands: bool) -> list[CommandDescriptor]:
|
|
192
|
+
"""收集指令,按需包含子指令。"""
|
|
193
|
+
descriptors: list[CommandDescriptor] = []
|
|
194
|
+
for handler in star_handlers_registry:
|
|
195
|
+
desc = _build_descriptor(handler)
|
|
196
|
+
if not desc:
|
|
197
|
+
continue
|
|
198
|
+
if not include_sub_commands and desc.is_sub_command:
|
|
199
|
+
continue
|
|
200
|
+
descriptors.append(desc)
|
|
201
|
+
return descriptors
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _build_descriptor(handler: StarHandlerMetadata) -> CommandDescriptor | None:
|
|
205
|
+
filter_ref = _locate_primary_filter(handler)
|
|
206
|
+
if filter_ref is None:
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
plugin_meta = star_map.get(handler.handler_module_path)
|
|
210
|
+
plugin_name = (
|
|
211
|
+
plugin_meta.name if plugin_meta else None
|
|
212
|
+
) or handler.handler_module_path
|
|
213
|
+
plugin_display = plugin_meta.display_name if plugin_meta else None
|
|
214
|
+
|
|
215
|
+
is_sub_command = bool(handler.extras_configs.get("sub_command"))
|
|
216
|
+
parent_group_handler = ""
|
|
217
|
+
|
|
218
|
+
if isinstance(filter_ref, CommandFilter):
|
|
219
|
+
raw_fragment = getattr(
|
|
220
|
+
filter_ref, "_original_command_name", filter_ref.command_name
|
|
221
|
+
)
|
|
222
|
+
current_fragment = filter_ref.command_name
|
|
223
|
+
parent_signature = (filter_ref.parent_command_names or [""])[0].strip()
|
|
224
|
+
# 如果是子指令,尝试找到父指令组的 handler_full_name
|
|
225
|
+
if is_sub_command and parent_signature:
|
|
226
|
+
parent_group_handler = _find_parent_group_handler(
|
|
227
|
+
handler.handler_module_path, parent_signature
|
|
228
|
+
)
|
|
229
|
+
else:
|
|
230
|
+
raw_fragment = getattr(
|
|
231
|
+
filter_ref, "_original_group_name", filter_ref.group_name
|
|
232
|
+
)
|
|
233
|
+
current_fragment = filter_ref.group_name
|
|
234
|
+
parent_signature = _resolve_group_parent_signature(filter_ref)
|
|
235
|
+
|
|
236
|
+
original_command = _compose_command(parent_signature, raw_fragment)
|
|
237
|
+
effective_command = _compose_command(parent_signature, current_fragment)
|
|
238
|
+
|
|
239
|
+
# 确定 command_type
|
|
240
|
+
if isinstance(filter_ref, CommandGroupFilter):
|
|
241
|
+
command_type = "group"
|
|
242
|
+
elif is_sub_command:
|
|
243
|
+
command_type = "sub_command"
|
|
244
|
+
else:
|
|
245
|
+
command_type = "command"
|
|
246
|
+
|
|
247
|
+
descriptor = CommandDescriptor(
|
|
248
|
+
handler=handler,
|
|
249
|
+
filter_ref=filter_ref,
|
|
250
|
+
handler_full_name=handler.handler_full_name,
|
|
251
|
+
handler_name=handler.handler_name,
|
|
252
|
+
plugin_name=plugin_name,
|
|
253
|
+
plugin_display_name=plugin_display,
|
|
254
|
+
module_path=handler.handler_module_path,
|
|
255
|
+
description=handler.desc or "",
|
|
256
|
+
command_type=command_type,
|
|
257
|
+
raw_command_name=raw_fragment,
|
|
258
|
+
current_fragment=current_fragment,
|
|
259
|
+
parent_signature=parent_signature,
|
|
260
|
+
parent_group_handler=parent_group_handler,
|
|
261
|
+
original_command=original_command,
|
|
262
|
+
effective_command=effective_command,
|
|
263
|
+
aliases=sorted(getattr(filter_ref, "alias", set())),
|
|
264
|
+
permission=_determine_permission(handler),
|
|
265
|
+
enabled=handler.enabled,
|
|
266
|
+
is_group=isinstance(filter_ref, CommandGroupFilter),
|
|
267
|
+
is_sub_command=is_sub_command,
|
|
268
|
+
reserved=plugin_meta.reserved if plugin_meta else False,
|
|
269
|
+
)
|
|
270
|
+
return descriptor
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _build_descriptor_by_full_name(full_name: str) -> CommandDescriptor | None:
|
|
274
|
+
handler = star_handlers_registry.get_handler_by_full_name(full_name)
|
|
275
|
+
if not handler:
|
|
276
|
+
return None
|
|
277
|
+
return _build_descriptor(handler)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _locate_primary_filter(
|
|
281
|
+
handler: StarHandlerMetadata,
|
|
282
|
+
) -> CommandFilter | CommandGroupFilter | None:
|
|
283
|
+
for filter_ref in handler.event_filters:
|
|
284
|
+
if isinstance(filter_ref, (CommandFilter, CommandGroupFilter)):
|
|
285
|
+
return filter_ref
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _determine_permission(handler: StarHandlerMetadata) -> str:
|
|
290
|
+
for filter_ref in handler.event_filters:
|
|
291
|
+
if isinstance(filter_ref, PermissionTypeFilter):
|
|
292
|
+
return (
|
|
293
|
+
"admin"
|
|
294
|
+
if filter_ref.permission_type == PermissionType.ADMIN
|
|
295
|
+
else "member"
|
|
296
|
+
)
|
|
297
|
+
return "everyone"
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _resolve_group_parent_signature(group_filter: CommandGroupFilter) -> str:
|
|
301
|
+
signatures: list[str] = []
|
|
302
|
+
parent = group_filter.parent_group
|
|
303
|
+
while parent:
|
|
304
|
+
signatures.append(getattr(parent, "_original_group_name", parent.group_name))
|
|
305
|
+
parent = parent.parent_group
|
|
306
|
+
return " ".join(reversed(signatures)).strip()
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _find_parent_group_handler(module_path: str, parent_signature: str) -> str:
|
|
310
|
+
"""根据模块路径和父级签名,找到对应的指令组 handler_full_name。"""
|
|
311
|
+
parent_sig_normalized = parent_signature.strip()
|
|
312
|
+
for handler in star_handlers_registry:
|
|
313
|
+
if handler.handler_module_path != module_path:
|
|
314
|
+
continue
|
|
315
|
+
filter_ref = _locate_primary_filter(handler)
|
|
316
|
+
if not isinstance(filter_ref, CommandGroupFilter):
|
|
317
|
+
continue
|
|
318
|
+
# 检查该指令组的完整指令名是否匹配 parent_signature
|
|
319
|
+
group_names = filter_ref.get_complete_command_names()
|
|
320
|
+
if parent_sig_normalized in group_names:
|
|
321
|
+
return handler.handler_full_name
|
|
322
|
+
return ""
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _compose_command(parent_signature: str, fragment: str | None) -> str:
|
|
326
|
+
fragment = (fragment or "").strip()
|
|
327
|
+
parent_signature = parent_signature.strip()
|
|
328
|
+
if not parent_signature:
|
|
329
|
+
return fragment
|
|
330
|
+
if not fragment:
|
|
331
|
+
return parent_signature
|
|
332
|
+
return f"{parent_signature} {fragment}"
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _bind_descriptor_with_config(
|
|
336
|
+
descriptor: CommandDescriptor,
|
|
337
|
+
config: CommandConfig,
|
|
338
|
+
) -> None:
|
|
339
|
+
_apply_config_to_descriptor(descriptor, config)
|
|
340
|
+
_apply_config_to_runtime(descriptor, config)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _apply_config_to_descriptor(
|
|
344
|
+
descriptor: CommandDescriptor,
|
|
345
|
+
config: CommandConfig,
|
|
346
|
+
) -> None:
|
|
347
|
+
descriptor.config = config
|
|
348
|
+
descriptor.enabled = config.enabled
|
|
349
|
+
|
|
350
|
+
if config.original_command:
|
|
351
|
+
descriptor.original_command = config.original_command
|
|
352
|
+
|
|
353
|
+
new_fragment = config.resolved_command or descriptor.current_fragment
|
|
354
|
+
descriptor.current_fragment = new_fragment
|
|
355
|
+
descriptor.effective_command = _compose_command(
|
|
356
|
+
descriptor.parent_signature,
|
|
357
|
+
new_fragment,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _apply_config_to_runtime(
|
|
362
|
+
descriptor: CommandDescriptor,
|
|
363
|
+
config: CommandConfig,
|
|
364
|
+
) -> None:
|
|
365
|
+
descriptor.handler.enabled = config.enabled
|
|
366
|
+
if descriptor.filter_ref and descriptor.current_fragment:
|
|
367
|
+
_set_filter_fragment(descriptor.filter_ref, descriptor.current_fragment)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _bind_configs_to_descriptors(
|
|
371
|
+
descriptors: list[CommandDescriptor],
|
|
372
|
+
config_records: list[CommandConfig],
|
|
373
|
+
) -> dict[str, CommandConfig]:
|
|
374
|
+
config_map = {cfg.handler_full_name: cfg for cfg in config_records}
|
|
375
|
+
for desc in descriptors:
|
|
376
|
+
if cfg := config_map.get(desc.handler_full_name):
|
|
377
|
+
_bind_descriptor_with_config(desc, cfg)
|
|
378
|
+
return config_map
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _group_conflicts(
|
|
382
|
+
descriptors: list[CommandDescriptor],
|
|
383
|
+
) -> dict[str, list[CommandDescriptor]]:
|
|
384
|
+
conflicts: dict[str, list[CommandDescriptor]] = defaultdict(list)
|
|
385
|
+
for desc in descriptors:
|
|
386
|
+
if desc.effective_command and desc.enabled:
|
|
387
|
+
conflicts[desc.effective_command].append(desc)
|
|
388
|
+
return {k: v for k, v in conflicts.items() if len(v) > 1}
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _set_filter_fragment(
|
|
392
|
+
filter_ref: CommandFilter | CommandGroupFilter,
|
|
393
|
+
fragment: str,
|
|
394
|
+
) -> None:
|
|
395
|
+
attr = (
|
|
396
|
+
"group_name" if isinstance(filter_ref, CommandGroupFilter) else "command_name"
|
|
397
|
+
)
|
|
398
|
+
current_value = getattr(filter_ref, attr)
|
|
399
|
+
if fragment == current_value:
|
|
400
|
+
return
|
|
401
|
+
setattr(filter_ref, attr, fragment)
|
|
402
|
+
if hasattr(filter_ref, "_cmpl_cmd_names"):
|
|
403
|
+
filter_ref._cmpl_cmd_names = None
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _is_command_in_use(
|
|
407
|
+
target_handler_full_name: str,
|
|
408
|
+
candidate_full_command: str,
|
|
409
|
+
) -> bool:
|
|
410
|
+
candidate = candidate_full_command.strip()
|
|
411
|
+
for handler in star_handlers_registry:
|
|
412
|
+
if handler.handler_full_name == target_handler_full_name:
|
|
413
|
+
continue
|
|
414
|
+
filter_ref = _locate_primary_filter(handler)
|
|
415
|
+
if not filter_ref:
|
|
416
|
+
continue
|
|
417
|
+
names = {name.strip() for name in filter_ref.get_complete_command_names()}
|
|
418
|
+
if candidate in names:
|
|
419
|
+
return True
|
|
420
|
+
return False
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _descriptor_to_dict(desc: CommandDescriptor) -> dict[str, Any]:
|
|
424
|
+
result = {
|
|
425
|
+
"handler_full_name": desc.handler_full_name,
|
|
426
|
+
"handler_name": desc.handler_name,
|
|
427
|
+
"plugin": desc.plugin_name,
|
|
428
|
+
"plugin_display_name": desc.plugin_display_name,
|
|
429
|
+
"module_path": desc.module_path,
|
|
430
|
+
"description": desc.description,
|
|
431
|
+
"type": desc.command_type,
|
|
432
|
+
"parent_signature": desc.parent_signature,
|
|
433
|
+
"parent_group_handler": desc.parent_group_handler,
|
|
434
|
+
"original_command": desc.original_command,
|
|
435
|
+
"current_fragment": desc.current_fragment,
|
|
436
|
+
"effective_command": desc.effective_command,
|
|
437
|
+
"aliases": desc.aliases,
|
|
438
|
+
"permission": desc.permission,
|
|
439
|
+
"enabled": desc.enabled,
|
|
440
|
+
"is_group": desc.is_group,
|
|
441
|
+
"has_conflict": desc.has_conflict,
|
|
442
|
+
"reserved": desc.reserved,
|
|
443
|
+
}
|
|
444
|
+
# 如果是指令组,包含子指令列表
|
|
445
|
+
if desc.is_group and desc.sub_commands:
|
|
446
|
+
result["sub_commands"] = [_descriptor_to_dict(sub) for sub in desc.sub_commands]
|
|
447
|
+
else:
|
|
448
|
+
result["sub_commands"] = []
|
|
449
|
+
return result
|
astrbot/core/star/context.py
CHANGED
|
@@ -267,6 +267,10 @@ class Context:
|
|
|
267
267
|
):
|
|
268
268
|
"""通过 ID 获取对应的 LLM Provider。"""
|
|
269
269
|
prov = self.provider_manager.inst_map.get(provider_id)
|
|
270
|
+
if provider_id and not prov:
|
|
271
|
+
logger.warning(
|
|
272
|
+
f"没有找到 ID 为 {provider_id} 的提供商,这可能是由于您修改了提供商(模型)ID 导致的。"
|
|
273
|
+
)
|
|
270
274
|
return prov
|
|
271
275
|
|
|
272
276
|
def get_all_providers(self) -> list[Provider]:
|
|
@@ -40,6 +40,7 @@ class CommandFilter(HandlerFilter):
|
|
|
40
40
|
):
|
|
41
41
|
self.command_name = command_name
|
|
42
42
|
self.alias = alias if alias else set()
|
|
43
|
+
self._original_command_name = command_name
|
|
43
44
|
self.parent_command_names = (
|
|
44
45
|
parent_command_names if parent_command_names is not None else [""]
|
|
45
46
|
)
|
|
@@ -18,6 +18,7 @@ class CommandGroupFilter(HandlerFilter):
|
|
|
18
18
|
):
|
|
19
19
|
self.group_name = group_name
|
|
20
20
|
self.alias = alias if alias else set()
|
|
21
|
+
self._original_group_name = group_name
|
|
21
22
|
self.sub_command_filters: list[CommandFilter | CommandGroupFilter] = []
|
|
22
23
|
self.custom_filter_list: list[CustomFilter] = []
|
|
23
24
|
self.parent_group = parent_group
|
|
@@ -118,6 +118,8 @@ class StarHandlerRegistry(Generic[T]):
|
|
|
118
118
|
# 过滤事件类型
|
|
119
119
|
if handler.event_type != event_type:
|
|
120
120
|
continue
|
|
121
|
+
if not handler.enabled:
|
|
122
|
+
continue
|
|
121
123
|
# 过滤启用状态
|
|
122
124
|
if only_activated:
|
|
123
125
|
plugin = star_map.get(handler.handler_module_path)
|
|
@@ -220,6 +222,8 @@ class StarHandlerMetadata(Generic[H]):
|
|
|
220
222
|
extras_configs: dict = field(default_factory=dict)
|
|
221
223
|
"""插件注册的一些其他的信息, 如 priority 等"""
|
|
222
224
|
|
|
225
|
+
enabled: bool = True
|
|
226
|
+
|
|
223
227
|
def __lt__(self, other: StarHandlerMetadata):
|
|
224
228
|
"""定义小于运算符以支持优先队列"""
|
|
225
229
|
return self.extras_configs.get("priority", 0) < other.extras_configs.get(
|
|
@@ -23,6 +23,7 @@ from astrbot.core.utils.astrbot_path import (
|
|
|
23
23
|
from astrbot.core.utils.io import remove_dir
|
|
24
24
|
|
|
25
25
|
from . import StarMetadata
|
|
26
|
+
from .command_management import sync_command_configs
|
|
26
27
|
from .context import Context
|
|
27
28
|
from .filter.permission import PermissionType, PermissionTypeFilter
|
|
28
29
|
from .star import star_map, star_registry
|
|
@@ -630,6 +631,7 @@ class PluginManager:
|
|
|
630
631
|
# 清除 pip.main 导致的多余的 logging handlers
|
|
631
632
|
for handler in logging.root.handlers[:]:
|
|
632
633
|
logging.root.removeHandler(handler)
|
|
634
|
+
await sync_command_configs()
|
|
633
635
|
|
|
634
636
|
if not fail_rec:
|
|
635
637
|
return True, None
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from typing import Literal, TypedDict
|
|
2
|
+
|
|
3
|
+
import aiohttp
|
|
4
|
+
|
|
5
|
+
from astrbot.core import logger
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LLMModalities(TypedDict):
|
|
9
|
+
input: list[Literal["text", "image", "audio", "video"]]
|
|
10
|
+
output: list[Literal["text", "image", "audio", "video"]]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LLMLimit(TypedDict):
|
|
14
|
+
context: int
|
|
15
|
+
output: int
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LLMMetadata(TypedDict):
|
|
19
|
+
id: str
|
|
20
|
+
reasoning: bool
|
|
21
|
+
tool_call: bool
|
|
22
|
+
knowledge: str
|
|
23
|
+
release_date: str
|
|
24
|
+
modalities: LLMModalities
|
|
25
|
+
open_weights: bool
|
|
26
|
+
limit: LLMLimit
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
LLM_METADATAS: dict[str, LLMMetadata] = {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def update_llm_metadata():
|
|
33
|
+
url = "https://models.dev/api.json"
|
|
34
|
+
try:
|
|
35
|
+
async with aiohttp.ClientSession() as session:
|
|
36
|
+
async with session.get(url) as response:
|
|
37
|
+
data = await response.json()
|
|
38
|
+
global LLM_METADATAS
|
|
39
|
+
models = {}
|
|
40
|
+
for info in data.values():
|
|
41
|
+
for model in info.get("models", {}).values():
|
|
42
|
+
model_id = model.get("id")
|
|
43
|
+
if not model_id:
|
|
44
|
+
continue
|
|
45
|
+
models[model_id] = LLMMetadata(
|
|
46
|
+
id=model_id,
|
|
47
|
+
reasoning=model.get("reasoning", False),
|
|
48
|
+
tool_call=model.get("tool_call", False),
|
|
49
|
+
knowledge=model.get("knowledge", "none"),
|
|
50
|
+
release_date=model.get("release_date", ""),
|
|
51
|
+
modalities=model.get(
|
|
52
|
+
"modalities", {"input": [], "output": []}
|
|
53
|
+
),
|
|
54
|
+
open_weights=model.get("open_weights", False),
|
|
55
|
+
limit=model.get("limit", {"context": 0, "output": 0}),
|
|
56
|
+
)
|
|
57
|
+
# Replace the global cache in-place so references remain valid
|
|
58
|
+
LLM_METADATAS.clear()
|
|
59
|
+
LLM_METADATAS.update(models)
|
|
60
|
+
logger.info(f"Successfully fetched metadata for {len(models)} LLMs.")
|
|
61
|
+
except Exception as e:
|
|
62
|
+
logger.error(f"Failed to fetch LLM metadata: {e}")
|
|
63
|
+
return
|
|
@@ -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())
|
|
@@ -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",
|