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
|
@@ -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
|
|
@@ -467,6 +468,18 @@ class PluginManager:
|
|
|
467
468
|
metadata.star_cls = metadata.star_cls_type(
|
|
468
469
|
context=self.context,
|
|
469
470
|
)
|
|
471
|
+
|
|
472
|
+
p_name = (metadata.name or "unknown").lower().replace("/", "_")
|
|
473
|
+
p_author = (
|
|
474
|
+
(metadata.author or "unknown").lower().replace("/", "_")
|
|
475
|
+
)
|
|
476
|
+
setattr(metadata.star_cls, "name", p_name)
|
|
477
|
+
setattr(metadata.star_cls, "author", p_author)
|
|
478
|
+
setattr(
|
|
479
|
+
metadata.star_cls,
|
|
480
|
+
"plugin_id",
|
|
481
|
+
f"{p_author}/{p_name}",
|
|
482
|
+
)
|
|
470
483
|
else:
|
|
471
484
|
logger.info(f"插件 {metadata.name} 已被禁用。")
|
|
472
485
|
|
|
@@ -618,6 +631,7 @@ class PluginManager:
|
|
|
618
631
|
# 清除 pip.main 导致的多余的 logging handlers
|
|
619
632
|
for handler in logging.root.handlers[:]:
|
|
620
633
|
logging.root.removeHandler(handler)
|
|
634
|
+
await sync_command_configs()
|
|
621
635
|
|
|
622
636
|
if not fail_rec:
|
|
623
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
|