AstrBot 4.7.3__py3-none-any.whl → 4.7.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/cli/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "4.7.3"
1
+ __version__ = "4.7.4"
@@ -3,7 +3,7 @@
3
3
 
4
4
  from typing import Any, ClassVar, Literal, cast
5
5
 
6
- from pydantic import BaseModel, GetCoreSchemaHandler
6
+ from pydantic import BaseModel, GetCoreSchemaHandler, model_validator
7
7
  from pydantic_core import core_schema
8
8
 
9
9
 
@@ -145,23 +145,39 @@ class Message(BaseModel):
145
145
  "tool",
146
146
  ]
147
147
 
148
- content: str | list[ContentPart]
148
+ content: str | list[ContentPart] | None = None
149
149
  """The content of the message."""
150
150
 
151
+ tool_calls: list[ToolCall] | list[dict] | None = None
152
+ """The tool calls of the message."""
153
+
154
+ tool_call_id: str | None = None
155
+ """The ID of the tool call."""
156
+
157
+ @model_validator(mode="after")
158
+ def check_content_required(self):
159
+ # assistant + tool_calls is not None: allow content to be None
160
+ if self.role == "assistant" and self.tool_calls is not None:
161
+ return self
162
+
163
+ # other all cases: content is required
164
+ if self.content is None:
165
+ raise ValueError(
166
+ "content is required unless role='assistant' and tool_calls is not None"
167
+ )
168
+ return self
169
+
151
170
 
152
171
  class AssistantMessageSegment(Message):
153
172
  """A message segment from the assistant."""
154
173
 
155
174
  role: Literal["assistant"] = "assistant"
156
- content: str | list[ContentPart] | None = None
157
- tool_calls: list[ToolCall] | list[dict] | None = None
158
175
 
159
176
 
160
177
  class ToolCallMessageSegment(Message):
161
178
  """A message segment representing a tool call."""
162
179
 
163
180
  role: Literal["tool"] = "tool"
164
- tool_call_id: str
165
181
 
166
182
 
167
183
  class UserMessageSegment(Message):
@@ -4,7 +4,7 @@ import os
4
4
 
5
5
  from astrbot.core.utils.astrbot_path import get_astrbot_data_path
6
6
 
7
- VERSION = "4.7.3"
7
+ VERSION = "4.7.4"
8
8
  DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
9
9
 
10
10
  # 默认配置
@@ -73,8 +73,14 @@ DEFAULT_CONFIG = {
73
73
  "coze_agent_runner_provider_id": "",
74
74
  "dashscope_agent_runner_provider_id": "",
75
75
  "unsupported_streaming_strategy": "realtime_segmenting",
76
+ "reachability_check": False,
76
77
  "max_agent_step": 30,
77
78
  "tool_call_timeout": 60,
79
+ "file_extract": {
80
+ "enable": False,
81
+ "provider": "moonshotai",
82
+ "moonshotai_api_key": "",
83
+ },
78
84
  },
79
85
  "provider_stt_settings": {
80
86
  "enable": False,
@@ -2068,6 +2074,20 @@ CONFIG_METADATA_2 = {
2068
2074
  "tool_call_timeout": {
2069
2075
  "type": "int",
2070
2076
  },
2077
+ "file_extract": {
2078
+ "type": "object",
2079
+ "items": {
2080
+ "enable": {
2081
+ "type": "bool",
2082
+ },
2083
+ "provider": {
2084
+ "type": "string",
2085
+ },
2086
+ "moonshotai_api_key": {
2087
+ "type": "string",
2088
+ },
2089
+ },
2090
+ },
2071
2091
  },
2072
2092
  },
2073
2093
  "provider_stt_settings": {
@@ -2402,6 +2422,36 @@ CONFIG_METADATA_3 = {
2402
2422
  "provider_settings.enable": True,
2403
2423
  },
2404
2424
  },
2425
+ # "file_extract": {
2426
+ # "description": "文档解析能力 [beta]",
2427
+ # "type": "object",
2428
+ # "items": {
2429
+ # "provider_settings.file_extract.enable": {
2430
+ # "description": "启用文档解析能力",
2431
+ # "type": "bool",
2432
+ # },
2433
+ # "provider_settings.file_extract.provider": {
2434
+ # "description": "文档解析提供商",
2435
+ # "type": "string",
2436
+ # "options": ["moonshotai"],
2437
+ # "condition": {
2438
+ # "provider_settings.file_extract.enable": True,
2439
+ # },
2440
+ # },
2441
+ # "provider_settings.file_extract.moonshotai_api_key": {
2442
+ # "description": "Moonshot AI API Key",
2443
+ # "type": "string",
2444
+ # "condition": {
2445
+ # "provider_settings.file_extract.provider": "moonshotai",
2446
+ # "provider_settings.file_extract.enable": True,
2447
+ # },
2448
+ # },
2449
+ # },
2450
+ # "condition": {
2451
+ # "provider_settings.agent_runner_type": "local",
2452
+ # "provider_settings.enable": True,
2453
+ # },
2454
+ # },
2405
2455
  "others": {
2406
2456
  "description": "其他配置",
2407
2457
  "type": "object",
@@ -2496,6 +2546,11 @@ CONFIG_METADATA_3 = {
2496
2546
  "description": "开启 TTS 时同时输出语音和文字内容",
2497
2547
  "type": "bool",
2498
2548
  },
2549
+ "provider_settings.reachability_check": {
2550
+ "description": "提供商可达性检测",
2551
+ "type": "bool",
2552
+ "hint": "/provider 命令列出模型时是否并发检测连通性。开启后会主动调用模型测试连通性,可能产生额外 token 消耗。",
2553
+ },
2499
2554
  },
2500
2555
  "condition": {
2501
2556
  "provider_settings.enable": True,
@@ -722,7 +722,12 @@ class File(BaseMessageComponent):
722
722
  """下载文件"""
723
723
  download_dir = os.path.join(get_astrbot_data_path(), "temp")
724
724
  os.makedirs(download_dir, exist_ok=True)
725
- file_path = os.path.join(download_dir, f"{uuid.uuid4().hex}")
725
+ if self.name:
726
+ name, ext = os.path.splitext(self.name)
727
+ filename = f"{name}_{uuid.uuid4().hex[:8]}{ext}"
728
+ else:
729
+ filename = f"{uuid.uuid4().hex}"
730
+ file_path = os.path.join(download_dir, filename)
726
731
  await download_file(self.url, file_path)
727
732
  self.file_ = os.path.abspath(file_path)
728
733
 
@@ -9,7 +9,7 @@ from astrbot.core import logger
9
9
  from astrbot.core.agent.tool import ToolSet
10
10
  from astrbot.core.astr_agent_context import AstrAgentContext
11
11
  from astrbot.core.conversation_mgr import Conversation
12
- from astrbot.core.message.components import Image
12
+ from astrbot.core.message.components import File, Image, Reply
13
13
  from astrbot.core.message.message_event_result import (
14
14
  MessageChain,
15
15
  MessageEventResult,
@@ -22,6 +22,7 @@ from astrbot.core.provider.entities import (
22
22
  ProviderRequest,
23
23
  )
24
24
  from astrbot.core.star.star_handler import EventType, star_map
25
+ from astrbot.core.utils.file_extract import extract_file_moonshotai
25
26
  from astrbot.core.utils.metrics import Metric
26
27
  from astrbot.core.utils.session_lock import session_lock_manager
27
28
 
@@ -56,6 +57,13 @@ class InternalAgentSubStage(Stage):
56
57
  self.show_reasoning = settings.get("display_reasoning_text", False)
57
58
  self.kb_agentic_mode: bool = conf.get("kb_agentic_mode", False)
58
59
 
60
+ file_extract_conf: dict = settings.get("file_extract", {})
61
+ self.file_extract_enabled: bool = file_extract_conf.get("enable", False)
62
+ self.file_extract_prov: str = file_extract_conf.get("provider", "moonshotai")
63
+ self.file_extract_msh_api_key: str = file_extract_conf.get(
64
+ "moonshotai_api_key", ""
65
+ )
66
+
59
67
  self.conv_manager = ctx.plugin_manager.context.conversation_manager
60
68
 
61
69
  def _select_provider(self, event: AstrMessageEvent):
@@ -114,6 +122,50 @@ class InternalAgentSubStage(Stage):
114
122
  req.func_tool = ToolSet()
115
123
  req.func_tool.add_tool(KNOWLEDGE_BASE_QUERY_TOOL)
116
124
 
125
+ async def _apply_file_extract(
126
+ self,
127
+ event: AstrMessageEvent,
128
+ req: ProviderRequest,
129
+ ):
130
+ """Apply file extract to the provider request"""
131
+ file_paths = []
132
+ file_names = []
133
+ for comp in event.message_obj.message:
134
+ if isinstance(comp, File):
135
+ file_paths.append(await comp.get_file())
136
+ file_names.append(comp.name)
137
+ elif isinstance(comp, Reply) and comp.chain:
138
+ for reply_comp in comp.chain:
139
+ if isinstance(reply_comp, File):
140
+ file_paths.append(await reply_comp.get_file())
141
+ file_names.append(reply_comp.name)
142
+ if not file_paths:
143
+ return
144
+ if not req.prompt:
145
+ req.prompt = "总结一下文件里面讲了什么?"
146
+ if self.file_extract_prov == "moonshotai":
147
+ if not self.file_extract_msh_api_key:
148
+ logger.error("Moonshot AI API key for file extract is not set")
149
+ return
150
+ file_contents = await asyncio.gather(
151
+ *[
152
+ extract_file_moonshotai(file_path, self.file_extract_msh_api_key)
153
+ for file_path in file_paths
154
+ ]
155
+ )
156
+ else:
157
+ logger.error(f"Unsupported file extract provider: {self.file_extract_prov}")
158
+ return
159
+
160
+ # add file extract results to contexts
161
+ for file_content, file_name in zip(file_contents, file_names):
162
+ req.contexts.append(
163
+ {
164
+ "role": "system",
165
+ "content": f"File Extract Results of user uploaded files:\n{file_content}\nFile Name: {file_name or 'Unknown'}",
166
+ },
167
+ )
168
+
117
169
  def _truncate_contexts(
118
170
  self,
119
171
  contexts: list[dict],
@@ -346,6 +398,17 @@ class InternalAgentSubStage(Stage):
346
398
 
347
399
  event.set_extra("provider_request", req)
348
400
 
401
+ # fix contexts json str
402
+ if isinstance(req.contexts, str):
403
+ req.contexts = json.loads(req.contexts)
404
+
405
+ # apply file extract
406
+ if self.file_extract_enabled:
407
+ try:
408
+ await self._apply_file_extract(event, req)
409
+ except Exception as e:
410
+ logger.error(f"Error occurred while applying file extract: {e}")
411
+
349
412
  if not req.prompt and not req.image_urls:
350
413
  return
351
414
 
@@ -356,10 +419,6 @@ class InternalAgentSubStage(Stage):
356
419
  # apply knowledge base feature
357
420
  await self._apply_kb(event, req)
358
421
 
359
- # fix contexts json str
360
- if isinstance(req.contexts, str):
361
- req.contexts = json.loads(req.contexts)
362
-
363
422
  # truncate contexts to fit max length
364
423
  if req.contexts:
365
424
  req.contexts = self._truncate_contexts(req.contexts)
@@ -246,7 +246,13 @@ class AiocqhttpAdapter(Platform):
246
246
  if m["data"].get("url") and m["data"].get("url").startswith("http"):
247
247
  # Lagrange
248
248
  logger.info("guessing lagrange")
249
- file_name = m["data"].get("file_name", "file")
249
+ # 检查多个可能的文件名字段
250
+ file_name = (
251
+ m["data"].get("file_name", "")
252
+ or m["data"].get("name", "")
253
+ or m["data"].get("file", "")
254
+ or "file"
255
+ )
250
256
  abm.message.append(File(name=file_name, url=m["data"]["url"]))
251
257
  else:
252
258
  try:
@@ -265,7 +271,14 @@ class AiocqhttpAdapter(Platform):
265
271
  )
266
272
  if ret and "url" in ret:
267
273
  file_url = ret["url"] # https
268
- a = File(name="", url=file_url)
274
+ # 优先从 API 返回值获取文件名,其次从原始消息数据获取
275
+ file_name = (
276
+ ret.get("file_name", "")
277
+ or ret.get("name", "")
278
+ or m["data"].get("file", "")
279
+ or m["data"].get("file_name", "")
280
+ )
281
+ a = File(name=file_name, url=file_url)
269
282
  abm.message.append(a)
270
283
  else:
271
284
  logger.error(f"获取文件失败: {ret}")
@@ -250,7 +250,7 @@ class DingtalkPlatformAdapter(Platform):
250
250
 
251
251
  async def terminate(self):
252
252
  def monkey_patch_close():
253
- raise Exception("Graceful shutdown")
253
+ raise KeyboardInterrupt("Graceful shutdown")
254
254
 
255
255
  self.client_.open_connection = monkey_patch_close
256
256
  await self.client_.websocket.close(code=1000, reason="Graceful shutdown")
@@ -381,7 +381,9 @@ class TelegramPlatformAdapter(Platform):
381
381
  f"Telegram document file_path is None, cannot save the file {file_name}.",
382
382
  )
383
383
  else:
384
- message.message.append(Comp.File(file=file_path, name=file_name))
384
+ message.message.append(
385
+ Comp.File(file=file_path, name=file_name, url=file_path)
386
+ )
385
387
 
386
388
  elif update.message.video:
387
389
  file = await update.message.video.get_file()
@@ -1,5 +1,6 @@
1
1
  import abc
2
2
  import asyncio
3
+ import os
3
4
  from collections.abc import AsyncGenerator
4
5
 
5
6
  from astrbot.core.agent.message import Message
@@ -11,6 +12,7 @@ from astrbot.core.provider.entities import (
11
12
  ToolCallsResult,
12
13
  )
13
14
  from astrbot.core.provider.register import provider_cls_map
15
+ from astrbot.core.utils.astrbot_path import get_astrbot_path
14
16
 
15
17
 
16
18
  class AbstractProvider(abc.ABC):
@@ -43,6 +45,14 @@ class AbstractProvider(abc.ABC):
43
45
  )
44
46
  return meta
45
47
 
48
+ async def test(self):
49
+ """test the provider is a
50
+
51
+ raises:
52
+ Exception: if the provider is not available
53
+ """
54
+ ...
55
+
46
56
 
47
57
  class Provider(AbstractProvider):
48
58
  """Chat Provider"""
@@ -165,6 +175,12 @@ class Provider(AbstractProvider):
165
175
 
166
176
  return dicts
167
177
 
178
+ async def test(self, timeout: float = 45.0):
179
+ await asyncio.wait_for(
180
+ self.text_chat(prompt="REPLY `PONG` ONLY"),
181
+ timeout=timeout,
182
+ )
183
+
168
184
 
169
185
  class STTProvider(AbstractProvider):
170
186
  def __init__(self, provider_config: dict, provider_settings: dict) -> None:
@@ -177,6 +193,14 @@ class STTProvider(AbstractProvider):
177
193
  """获取音频的文本"""
178
194
  raise NotImplementedError
179
195
 
196
+ async def test(self):
197
+ sample_audio_path = os.path.join(
198
+ get_astrbot_path(),
199
+ "samples",
200
+ "stt_health_check.wav",
201
+ )
202
+ await self.get_text(sample_audio_path)
203
+
180
204
 
181
205
  class TTSProvider(AbstractProvider):
182
206
  def __init__(self, provider_config: dict, provider_settings: dict) -> None:
@@ -189,6 +213,9 @@ class TTSProvider(AbstractProvider):
189
213
  """获取文本的音频,返回音频文件路径"""
190
214
  raise NotImplementedError
191
215
 
216
+ async def test(self):
217
+ await self.get_audio("hi")
218
+
192
219
 
193
220
  class EmbeddingProvider(AbstractProvider):
194
221
  def __init__(self, provider_config: dict, provider_settings: dict) -> None:
@@ -211,6 +238,9 @@ class EmbeddingProvider(AbstractProvider):
211
238
  """获取向量的维度"""
212
239
  ...
213
240
 
241
+ async def test(self):
242
+ await self.get_embedding("astrbot")
243
+
214
244
  async def get_embeddings_batch(
215
245
  self,
216
246
  texts: list[str],
@@ -294,3 +324,8 @@ class RerankProvider(AbstractProvider):
294
324
  ) -> list[RerankResult]:
295
325
  """获取查询和文档的重排序分数"""
296
326
  ...
327
+
328
+ async def test(self):
329
+ result = await self.rerank("Apple", documents=["apple", "banana"])
330
+ if not result:
331
+ raise Exception("Rerank provider test failed, no results returned")
@@ -0,0 +1,23 @@
1
+ from pathlib import Path
2
+
3
+ from openai import AsyncOpenAI
4
+
5
+
6
+ async def extract_file_moonshotai(file_path: str, api_key: str) -> str:
7
+ """Extract text from a file using Moonshot AI API"""
8
+ """
9
+ Args:
10
+ file_path: The path to the file to extract text from
11
+ api_key: The API key to use to extract text from the file
12
+ Returns:
13
+ The text extracted from the file
14
+ """
15
+ client = AsyncOpenAI(
16
+ api_key=api_key,
17
+ base_url="https://api.moonshot.cn/v1",
18
+ )
19
+ file_object = await client.files.create(
20
+ file=Path(file_path),
21
+ purpose="file-extract", # type: ignore
22
+ )
23
+ return (await client.files.content(file_id=file_object.id)).text
@@ -18,11 +18,8 @@ from astrbot.core.config.i18n_utils import ConfigMetadataI18n
18
18
  from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
19
19
  from astrbot.core.platform.register import platform_cls_map, platform_registry
20
20
  from astrbot.core.provider import Provider
21
- from astrbot.core.provider.entities import ProviderType
22
- from astrbot.core.provider.provider import RerankProvider
23
21
  from astrbot.core.provider.register import provider_registry
24
22
  from astrbot.core.star.star import star_registry
25
- from astrbot.core.utils.astrbot_path import get_astrbot_path
26
23
 
27
24
  from .route import Response, Route, RouteContext
28
25
 
@@ -356,169 +353,20 @@ class ConfigRoute(Route):
356
353
  f"Attempting to check provider: {status_info['name']} (ID: {status_info['id']}, Type: {status_info['type']}, Model: {status_info['model']})",
357
354
  )
358
355
 
359
- if provider_capability_type == ProviderType.CHAT_COMPLETION:
360
- try:
361
- logger.debug(f"Sending 'Ping' to provider: {status_info['name']}")
362
- response = await asyncio.wait_for(
363
- provider.text_chat(prompt="REPLY `PONG` ONLY"),
364
- timeout=45.0,
365
- )
366
- logger.debug(
367
- f"Received response from {status_info['name']}: {response}",
368
- )
369
- if response is not None:
370
- status_info["status"] = "available"
371
- response_text_snippet = ""
372
- if (
373
- hasattr(response, "completion_text")
374
- and response.completion_text
375
- ):
376
- response_text_snippet = (
377
- response.completion_text[:70] + "..."
378
- if len(response.completion_text) > 70
379
- else response.completion_text
380
- )
381
- elif hasattr(response, "result_chain") and response.result_chain:
382
- try:
383
- response_text_snippet = (
384
- response.result_chain.get_plain_text()[:70] + "..."
385
- if len(response.result_chain.get_plain_text()) > 70
386
- else response.result_chain.get_plain_text()
387
- )
388
- except Exception as _:
389
- pass
390
- logger.info(
391
- f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{response_text_snippet}'",
392
- )
393
- else:
394
- status_info["error"] = (
395
- "Test call returned None, but expected an LLMResponse object."
396
- )
397
- logger.warning(
398
- f"Provider {status_info['name']} (ID: {status_info['id']}) test call returned None.",
399
- )
400
-
401
- except asyncio.TimeoutError:
402
- status_info["error"] = (
403
- "Connection timed out after 45 seconds during test call."
404
- )
405
- logger.warning(
406
- f"Provider {status_info['name']} (ID: {status_info['id']}) timed out.",
407
- )
408
- except Exception as e:
409
- error_message = str(e)
410
- status_info["error"] = error_message
411
- logger.warning(
412
- f"Provider {status_info['name']} (ID: {status_info['id']}) is unavailable. Error: {error_message}",
413
- )
414
- logger.debug(
415
- f"Traceback for {status_info['name']}:\n{traceback.format_exc()}",
416
- )
417
-
418
- elif provider_capability_type == ProviderType.EMBEDDING:
419
- try:
420
- # For embedding, we can call the get_embedding method with a short prompt.
421
- embedding_result = await provider.get_embedding("health_check")
422
- if isinstance(embedding_result, list) and (
423
- not embedding_result or isinstance(embedding_result[0], float)
424
- ):
425
- status_info["status"] = "available"
426
- else:
427
- status_info["status"] = "unavailable"
428
- status_info["error"] = (
429
- f"Embedding test failed: unexpected result type {type(embedding_result)}"
430
- )
431
- except Exception as e:
432
- logger.error(
433
- f"Error testing embedding provider {provider_name}: {e}",
434
- exc_info=True,
435
- )
436
- status_info["status"] = "unavailable"
437
- status_info["error"] = f"Embedding test failed: {e!s}"
438
-
439
- elif provider_capability_type == ProviderType.TEXT_TO_SPEECH:
440
- try:
441
- # For TTS, we can call the get_audio method with a short prompt.
442
- audio_result = await provider.get_audio("你好")
443
- if isinstance(audio_result, str) and audio_result:
444
- status_info["status"] = "available"
445
- else:
446
- status_info["status"] = "unavailable"
447
- status_info["error"] = (
448
- f"TTS test failed: unexpected result type {type(audio_result)}"
449
- )
450
- except Exception as e:
451
- logger.error(
452
- f"Error testing TTS provider {provider_name}: {e}",
453
- exc_info=True,
454
- )
455
- status_info["status"] = "unavailable"
456
- status_info["error"] = f"TTS test failed: {e!s}"
457
- elif provider_capability_type == ProviderType.SPEECH_TO_TEXT:
458
- try:
459
- logger.debug(
460
- f"Sending health check audio to provider: {status_info['name']}",
461
- )
462
- sample_audio_path = os.path.join(
463
- get_astrbot_path(),
464
- "samples",
465
- "stt_health_check.wav",
466
- )
467
- if not os.path.exists(sample_audio_path):
468
- status_info["status"] = "unavailable"
469
- status_info["error"] = (
470
- "STT test failed: sample audio file not found."
471
- )
472
- logger.warning(
473
- f"STT test for {status_info['name']} failed: sample audio file not found at {sample_audio_path}",
474
- )
475
- else:
476
- text_result = await provider.get_text(sample_audio_path)
477
- if isinstance(text_result, str) and text_result:
478
- status_info["status"] = "available"
479
- snippet = (
480
- text_result[:70] + "..."
481
- if len(text_result) > 70
482
- else text_result
483
- )
484
- logger.info(
485
- f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{snippet}'",
486
- )
487
- else:
488
- status_info["status"] = "unavailable"
489
- status_info["error"] = (
490
- f"STT test failed: unexpected result type {type(text_result)}"
491
- )
492
- logger.warning(
493
- f"STT test for {status_info['name']} failed: unexpected result type {type(text_result)}",
494
- )
495
- except Exception as e:
496
- logger.error(
497
- f"Error testing STT provider {provider_name}: {e}",
498
- exc_info=True,
499
- )
500
- status_info["status"] = "unavailable"
501
- status_info["error"] = f"STT test failed: {e!s}"
502
- elif provider_capability_type == ProviderType.RERANK:
503
- try:
504
- assert isinstance(provider, RerankProvider)
505
- await provider.rerank("Apple", documents=["apple", "banana"])
506
- status_info["status"] = "available"
507
- except Exception as e:
508
- logger.error(
509
- f"Error testing rerank provider {provider_name}: {e}",
510
- exc_info=True,
511
- )
512
- status_info["status"] = "unavailable"
513
- status_info["error"] = f"Rerank test failed: {e!s}"
514
-
515
- else:
516
- logger.debug(
517
- f"Provider {provider_name} is not a Chat Completion or Embedding provider. Marking as available without test. Meta: {meta}",
518
- )
356
+ try:
357
+ await provider.test()
519
358
  status_info["status"] = "available"
520
- status_info["error"] = (
521
- "This provider type is not tested and is assumed to be available."
359
+ logger.info(
360
+ f"Provider {status_info['name']} (ID: {status_info['id']}) is available.",
361
+ )
362
+ except Exception as e:
363
+ error_message = str(e)
364
+ status_info["error"] = error_message
365
+ logger.warning(
366
+ f"Provider {status_info['name']} (ID: {status_info['id']}) is unavailable. Error: {error_message}",
367
+ )
368
+ logger.debug(
369
+ f"Traceback for {status_info['name']}:\n{traceback.format_exc()}",
522
370
  )
523
371
 
524
372
  return status_info
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  import json
2
3
  import os
3
4
  import ssl
@@ -19,6 +20,10 @@ from astrbot.core.star.star_manager import PluginManager
19
20
 
20
21
  from .route import Response, Route, RouteContext
21
22
 
23
+ PLUGIN_UPDATE_CONCURRENCY = (
24
+ 3 # limit concurrent updates to avoid overwhelming plugin sources
25
+ )
26
+
22
27
 
23
28
  class PluginRoute(Route):
24
29
  def __init__(
@@ -33,6 +38,7 @@ class PluginRoute(Route):
33
38
  "/plugin/install": ("POST", self.install_plugin),
34
39
  "/plugin/install-upload": ("POST", self.install_plugin_upload),
35
40
  "/plugin/update": ("POST", self.update_plugin),
41
+ "/plugin/update-all": ("POST", self.update_all_plugins),
36
42
  "/plugin/uninstall": ("POST", self.uninstall_plugin),
37
43
  "/plugin/market_list": ("GET", self.get_online_plugins),
38
44
  "/plugin/off": ("POST", self.off_plugin),
@@ -63,7 +69,7 @@ class PluginRoute(Route):
63
69
  .__dict__
64
70
  )
65
71
 
66
- data = await request.json
72
+ data = await request.get_json()
67
73
  plugin_name = data.get("name", None)
68
74
  try:
69
75
  success, message = await self.plugin_manager.reload(plugin_name)
@@ -346,7 +352,7 @@ class PluginRoute(Route):
346
352
  .__dict__
347
353
  )
348
354
 
349
- post_data = await request.json
355
+ post_data = await request.get_json()
350
356
  repo_url = post_data["url"]
351
357
 
352
358
  proxy: str = post_data.get("proxy", None)
@@ -393,7 +399,7 @@ class PluginRoute(Route):
393
399
  .__dict__
394
400
  )
395
401
 
396
- post_data = await request.json
402
+ post_data = await request.get_json()
397
403
  plugin_name = post_data["name"]
398
404
  delete_config = post_data.get("delete_config", False)
399
405
  delete_data = post_data.get("delete_data", False)
@@ -418,7 +424,7 @@ class PluginRoute(Route):
418
424
  .__dict__
419
425
  )
420
426
 
421
- post_data = await request.json
427
+ post_data = await request.get_json()
422
428
  plugin_name = post_data["name"]
423
429
  proxy: str = post_data.get("proxy", None)
424
430
  try:
@@ -432,6 +438,59 @@ class PluginRoute(Route):
432
438
  logger.error(f"/api/plugin/update: {traceback.format_exc()}")
433
439
  return Response().error(str(e)).__dict__
434
440
 
441
+ async def update_all_plugins(self):
442
+ if DEMO_MODE:
443
+ return (
444
+ Response()
445
+ .error("You are not permitted to do this operation in demo mode")
446
+ .__dict__
447
+ )
448
+
449
+ post_data = await request.get_json()
450
+ plugin_names: list[str] = post_data.get("names") or []
451
+ proxy: str = post_data.get("proxy", "")
452
+
453
+ if not isinstance(plugin_names, list) or not plugin_names:
454
+ return Response().error("插件列表不能为空").__dict__
455
+
456
+ results = []
457
+ sem = asyncio.Semaphore(PLUGIN_UPDATE_CONCURRENCY)
458
+
459
+ async def _update_one(name: str):
460
+ async with sem:
461
+ try:
462
+ logger.info(f"批量更新插件 {name}")
463
+ await self.plugin_manager.update_plugin(name, proxy)
464
+ return {"name": name, "status": "ok", "message": "更新成功"}
465
+ except Exception as e:
466
+ logger.error(
467
+ f"/api/plugin/update-all: 更新插件 {name} 失败: {traceback.format_exc()}",
468
+ )
469
+ return {"name": name, "status": "error", "message": str(e)}
470
+
471
+ raw_results = await asyncio.gather(
472
+ *(_update_one(name) for name in plugin_names),
473
+ return_exceptions=True,
474
+ )
475
+ for name, result in zip(plugin_names, raw_results):
476
+ if isinstance(result, asyncio.CancelledError):
477
+ raise result
478
+ if isinstance(result, BaseException):
479
+ results.append(
480
+ {"name": name, "status": "error", "message": str(result)}
481
+ )
482
+ else:
483
+ results.append(result)
484
+
485
+ failed = [r for r in results if r["status"] == "error"]
486
+ message = (
487
+ "批量更新完成,全部成功。"
488
+ if not failed
489
+ else f"批量更新完成,其中 {len(failed)}/{len(results)} 个插件失败。"
490
+ )
491
+
492
+ return Response().ok({"results": results}, message).__dict__
493
+
435
494
  async def off_plugin(self):
436
495
  if DEMO_MODE:
437
496
  return (
@@ -440,7 +499,7 @@ class PluginRoute(Route):
440
499
  .__dict__
441
500
  )
442
501
 
443
- post_data = await request.json
502
+ post_data = await request.get_json()
444
503
  plugin_name = post_data["name"]
445
504
  try:
446
505
  await self.plugin_manager.turn_off_plugin(plugin_name)
@@ -458,7 +517,7 @@ class PluginRoute(Route):
458
517
  .__dict__
459
518
  )
460
519
 
461
- post_data = await request.json
520
+ post_data = await request.get_json()
462
521
  plugin_name = post_data["name"]
463
522
  try:
464
523
  await self.plugin_manager.turn_on_plugin(plugin_name)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: AstrBot
3
- Version: 4.7.3
3
+ Version: 4.7.4
4
4
  Summary: Easy-to-use multi-platform LLM chatbot and development framework
5
5
  License-File: LICENSE
6
6
  Keywords: Astrbot,Astrbot Module,Astrbot Plugin
@@ -8,7 +8,7 @@ astrbot/api/platform/__init__.py,sha256=HXvAy_KLtOJspoGVgDtLa7VjIZoF6WK3Puww55yy
8
8
  astrbot/api/provider/__init__.py,sha256=mJVcon0snjn_xYirk2hntwba6ymIYYC-ZKKmxvx-jok,379
9
9
  astrbot/api/star/__init__.py,sha256=OxgHGtWn3lEQGjb4twbpbWnRepUevPu7gxtDAkAsfhQ,250
10
10
  astrbot/api/util/__init__.py,sha256=L1O_mFEUDk8V4lEPsT5iiNbIiOVh7HbrNmitqzUWMZg,180
11
- astrbot/cli/__init__.py,sha256=AVNyT1NGAIO5hXAir4yWNPjObYD-xdsUWAPTcyEnMD0,22
11
+ astrbot/cli/__init__.py,sha256=-2lx1WPwUObUFzPGAM895DmnvFgEcMbD4Jj9dMh8L5c,22
12
12
  astrbot/cli/__main__.py,sha256=QobgMyFoLNTgI_OYddhGOZ9ZvQeBVjjz79mA2cC2OEU,1758
13
13
  astrbot/cli/commands/__init__.py,sha256=eAgppZQIqFO1ylQJFABeYrzQ0oZqPWjtE80aKIPB3ks,149
14
14
  astrbot/cli/commands/cmd_conf.py,sha256=6-YLicBt_zjWTzaVLUJ1VQLQPbDEPYACB9IVnN8Zvng,6330
@@ -41,7 +41,7 @@ astrbot/core/agent/agent.py,sha256=wquvKo18JcsJM56dwKyFFAoGhc5qLyQaeqdZ-LlZsWQ,3
41
41
  astrbot/core/agent/handoff.py,sha256=AxO0yx4Uscy0CO-3Q3fvDOfpfr3gUscLRplH7gH7-Lc,1194
42
42
  astrbot/core/agent/hooks.py,sha256=ooe9uUz7czlVt2W7jTDwkRbI-qDrOOXWjCaXtiAkrvE,830
43
43
  astrbot/core/agent/mcp_client.py,sha256=u52GPgpeZ1DCCVbI6x4kOGyodD_kweB8H1OgaVg-BZs,14085
44
- astrbot/core/agent/message.py,sha256=HqOuk6PTbuCZaMz4O5vr-BOYvfISoG5YujMejW78klc,5387
44
+ astrbot/core/agent/message.py,sha256=lSP67PoDzzcwcu_R0vcqVcTsnLmpn8SlV_Gkg3pzRRc,5931
45
45
  astrbot/core/agent/response.py,sha256=ddJABXu03Uw3b-YGTvRafFLJEGa40o93pIEz_CRTb4g,261
46
46
  astrbot/core/agent/run_context.py,sha256=h-teucYKYi5o4oTbAsIlkaa04yn2OSNC-ahIF2n6cwE,719
47
47
  astrbot/core/agent/tool.py,sha256=3F-zcADIJkACNljrlDJBZZCJwqhxFkfpgoKvg5v0TQM,9276
@@ -56,7 +56,7 @@ astrbot/core/agent/runners/dify/dify_agent_runner.py,sha256=LYwpjOcBWf3XlwNVzrDv
56
56
  astrbot/core/agent/runners/dify/dify_api_client.py,sha256=OXukDVgNx3VmYw6OCzjXyP8JmDWEFuy81sD9XnC4VRo,6530
57
57
  astrbot/core/config/__init__.py,sha256=vZjtpC7vr-IvBgSUtbS04C0wpulmCG5tPmcEP1WYE_4,172
58
58
  astrbot/core/config/astrbot_config.py,sha256=nGyvHyR9VJH9Pk0XKYyeDFVxjwbyVb9u0lIsuvpe3fg,6276
59
- astrbot/core/config/default.py,sha256=E5_DXdDvwf835zGmAvcBLTOiH5Z42fzBd6SrC8T6yrE,143667
59
+ astrbot/core/config/default.py,sha256=EkY1L104przDcn11NozIQgFAQZooQq4s464mZ0hp180,146181
60
60
  astrbot/core/config/i18n_utils.py,sha256=T2uLmhx1nohJIou14QQBjb2TSvdxDxtfUjVHpwy13z0,3841
61
61
  astrbot/core/db/__init__.py,sha256=s4oIWazGk2U1-9dkr3bvq8M4g9nwOXy4e3f53zlvAJk,10326
62
62
  astrbot/core/db/po.py,sha256=zFv5eU4tuM6E4ehGzugHnszWl6VBpS_rcD9IjjjkhXE,9398
@@ -94,7 +94,7 @@ astrbot/core/knowledge_base/retrieval/hit_stopwords.txt,sha256=8LikiRxpjLdAfJYyO
94
94
  astrbot/core/knowledge_base/retrieval/manager.py,sha256=m_aphslIVJE8f7t9M9K32vS8-Ll4BO0Chnd0zdQae1M,8417
95
95
  astrbot/core/knowledge_base/retrieval/rank_fusion.py,sha256=OeE4dSAsmyCt_ssAnMf7CQ4tQHlBFxjePgXVf_YwHmY,4441
96
96
  astrbot/core/knowledge_base/retrieval/sparse_retriever.py,sha256=sOCZ9I636j3P5WGxjKXVu7Amp-2DB9jQVQn96Ff7asI,3815
97
- astrbot/core/message/components.py,sha256=lQhxsfRPVMFFDdpCbyWjVN-uXyHIzDOc-3WLWoBAoL8,25526
97
+ astrbot/core/message/components.py,sha256=Cq8G8XsCY2PePvpCTCDbmaY4P8SSaMimb8nox8ZxUts,25707
98
98
  astrbot/core/message/message_event_result.py,sha256=1jC6NLeO7lzcTCi2NO28057sWpTsDabICkmGsiggyhk,7204
99
99
  astrbot/core/pipeline/__init__.py,sha256=nEepE3tnYYo0EYnzdLLyrp_k7XYg0LpQ3W6QuEeGL6k,1461
100
100
  astrbot/core/pipeline/context.py,sha256=jfEyX9i34XM7uqeqqK6rKRDgBXfsV9UHgRpf9Wj_hnI,505
@@ -111,7 +111,7 @@ astrbot/core/pipeline/process_stage/stage.py,sha256=J01Xg8sNFFGU4Z6dZy6jJcQnwXfd
111
111
  astrbot/core/pipeline/process_stage/utils.py,sha256=q4V5G0PZD5b5mPh1lM-6w79LKGpp7RR7-PqYFhWpopM,4061
112
112
  astrbot/core/pipeline/process_stage/method/agent_request.py,sha256=GlGrGCsCASC4a3PpG6Oc1907aLdl_PrUMXrFiEiEEzc,2043
113
113
  astrbot/core/pipeline/process_stage/method/star_request.py,sha256=icx3zkXLHDtX4SU5soe0UiOLgJjYW6ehoSBpMYSEE8U,2508
114
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py,sha256=ntW4aBAzMrzuquGuR2lLkneZ5X4j28yrOBDbVKWF7Ns,18686
114
+ astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py,sha256=sDU4b85nV7LswGngFeYOG9VUv-BRR7gufy00ptU_Utg,21175
115
115
  astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py,sha256=Cr5isDXIaL4oFlkt2Mr6UuijPNfp_RjC6RiTtwuSpfg,7329
116
116
  astrbot/core/pipeline/rate_limit_check/stage.py,sha256=9EVJ0zYtxATFsj7ADyWDYcSGBRqmrMiKWp1kkD9LONI,3962
117
117
  astrbot/core/pipeline/respond/stage.py,sha256=im_UrME-g-YOk9TnrDFzFeYIvzZsGBy9BDCAp5PXuNM,10738
@@ -129,8 +129,8 @@ astrbot/core/platform/platform.py,sha256=UETCazEPfEfQq0utZSMOIKcgIvQ3XbKv2yldwvt
129
129
  astrbot/core/platform/platform_metadata.py,sha256=b10aFNvC9HFYBJbedlaUxerLUyeVAOqvVksh2yE-s-M,707
130
130
  astrbot/core/platform/register.py,sha256=KiAMpiuEP6H5RwR9ItOgQEth02urvasKhObjiy5X-Hc,1956
131
131
  astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py,sha256=wuHJZu_Q86KQ83vaj_V-t3u5P7JIBURNtnFmYfCU4wM,8080
132
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py,sha256=jfvff0MhNcBgOJ6tlGMV7unxWzteFMwpVVKFAG72cjQ,17054
133
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py,sha256=70JxfF4IlUosYB9Y7Vg1s4-6-oAju9Z-daidByduI_8,9190
132
+ astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py,sha256=dwiynQ1iytm7v3gdK2uBWNUJntzQ5qF40f2opF2bfEM,17772
133
+ astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py,sha256=wWlOEuYuJ7jdbSr8P7yuwN8-xm0bDAegEYkPKBX_6R8,9198
134
134
  astrbot/core/platform/sources/dingtalk/dingtalk_event.py,sha256=Ph8AP03qct0dFDHKB1be0VY8Slxe9yiwqMsd7Zhpvas,2696
135
135
  astrbot/core/platform/sources/discord/client.py,sha256=dYu9ANCMeI9nY1K2FRquPOwTe1xLWHuVYslSLXoXNkY,4622
136
136
  astrbot/core/platform/sources/discord/components.py,sha256=sh0vvqKcO1a293XNApcA-olunPTHbVWFauq-Nwvj-o4,3957
@@ -152,7 +152,7 @@ astrbot/core/platform/sources/satori/satori_event.py,sha256=agI9FYUwBZM2CnxPANxD
152
152
  astrbot/core/platform/sources/slack/client.py,sha256=opkwKKwgaLxw3EpYILLAf7gzzC6KwpiUoq3TOewolh4,5633
153
153
  astrbot/core/platform/sources/slack/slack_adapter.py,sha256=LUZY-A-fVD8y5mG_86nQJjgKZ2j8k5uiue1B5vqjBhg,16134
154
154
  astrbot/core/platform/sources/slack/slack_event.py,sha256=qtd7AkYDrX5-rcf6-N3QX6Lm3YzvO8ipsUL8iFF6T_U,8926
155
- astrbot/core/platform/sources/telegram/tg_adapter.py,sha256=5eGR5PK1wUSDIBZQw7IW7JpfNfjSB3zldpVDpMs7zUo,16076
155
+ astrbot/core/platform/sources/telegram/tg_adapter.py,sha256=yNpzDpQlyWKyFhUNUHs7Tz_g3XT1yhEwuUK84QbCUpI,16129
156
156
  astrbot/core/platform/sources/telegram/tg_event.py,sha256=Xh8IDfSLtjJqzyiZVX05LKT8adm7Q2YPUHHKIZ-rpI4,11641
157
157
  astrbot/core/platform/sources/webchat/webchat_adapter.py,sha256=cQAkwFWyVsk5BLdQ7d82APfSea2q-6AZWldq9Q2FayU,6163
158
158
  astrbot/core/platform/sources/webchat/webchat_event.py,sha256=iXkHFugb0ecwWjuKgoiiqjFA7nM8iWgVwbb7HWlLKcY,5720
@@ -180,7 +180,7 @@ astrbot/core/provider/entites.py,sha256=0eYiQ-xttqFTb3WZR2b1oemdZy3d5sevELvj9Fix
180
180
  astrbot/core/provider/entities.py,sha256=AwD8KOG8dSlfQXSc1n8gx4xv2vW4niZDiaMoO1Y_dOw,12533
181
181
  astrbot/core/provider/func_tool_manager.py,sha256=28fOKbpWOxiclwfcNkmP6sHSBlK4cZKwPXyNhFjjXps,21181
182
182
  astrbot/core/provider/manager.py,sha256=_nXzEqWrOnIq-O845ECMJ79ez0TKQGzVJtRoYX8lCdo,22902
183
- astrbot/core/provider/provider.py,sha256=bMJBBZzNwHhVv8dEiO1tNUclzxS78Yb01vUbceHZXKA,10680
183
+ astrbot/core/provider/provider.py,sha256=BED65O2i4X1uArYGqNCHa6Yc_q1zJ5Vljcw_9zkCTDY,11641
184
184
  astrbot/core/provider/register.py,sha256=0WMYrT9vbRjeq-72HD0oRT45kJmeKA96UgSItpTJbX8,1904
185
185
  astrbot/core/provider/sources/anthropic_source.py,sha256=oSd7I8pCGwrmTKMQuxVza0vRNqgQw7nqhMxuZnc9ro0,15586
186
186
  astrbot/core/provider/sources/azure_tts_source.py,sha256=uhPr4Dja6XANesAmrfZiuINNIlXej0NV7Goo8ahMm14,9387
@@ -230,6 +230,7 @@ astrbot/core/star/register/star.py,sha256=Eto7nD_HFuCNt-VJnXUXKv2D7a5TQ6qkhzLJ_i
230
230
  astrbot/core/star/register/star_handler.py,sha256=kCIQVzI5EBWVAugkEsXHLTOoOQp3r5uTLYc4sPfdZls,17483
231
231
  astrbot/core/utils/astrbot_path.py,sha256=tQFD55EFwGbpR1tpoJADpdElPS5iTFAZVLIxCVvKypY,1213
232
232
  astrbot/core/utils/command_parser.py,sha256=Cwd4zzyKEoC-er0a-9WZ5n2g4F8eH9p6BHxD96gjaVM,617
233
+ astrbot/core/utils/file_extract.py,sha256=I9jgcaPYK74-BwuI18oUpoupnPYINeP3QFD3kFodqBA,697
233
234
  astrbot/core/utils/io.py,sha256=oowzwkZC76-BKofm6vI2sJnrg6IgJubcmvoaSvX2AsM,10815
234
235
  astrbot/core/utils/log_pipe.py,sha256=jphGRAdmzhBVRKdpJwOP1AtpbGlun9v7Cr50kHZtlyo,883
235
236
  astrbot/core/utils/metrics.py,sha256=CxEkdV2gJai0mw1IbL4DJ81WiQ5mY7v9M_-T6UtRJDs,2427
@@ -253,13 +254,13 @@ astrbot/dashboard/utils.py,sha256=KrAv0lnPaVR0bx8yevT1CLGbSNsJizlfkKkPEtVVANI,53
253
254
  astrbot/dashboard/routes/__init__.py,sha256=IKg0EzasXsd-OleSixE54Ul5wQcBeMHzVwhhjMFZ2dE,783
254
255
  astrbot/dashboard/routes/auth.py,sha256=rYkvt3MpCY9BhWjG0DUoX3YaBkJT1Id7M2pKqTmXbvo,2946
255
256
  astrbot/dashboard/routes/chat.py,sha256=v1nfoq3jSqiUnVZd9DilvPDgKz6_kIo8WdUG_dwZkXk,15525
256
- astrbot/dashboard/routes/config.py,sha256=IsSEl8flf1M79LTgUgMweewYaHQrr5Fxze335EkTXro,40535
257
+ astrbot/dashboard/routes/config.py,sha256=W6hAtplTlb5_Q6JlflXl3OXMU5peNui16nBXGUQBWBw,32997
257
258
  astrbot/dashboard/routes/conversation.py,sha256=sFHgkpNDdTR9qkSOC_JfSjzkfTuv63iaMxvh52wQUzM,10773
258
259
  astrbot/dashboard/routes/file.py,sha256=gULvXP9PnVOQlyv_PCEzZQE5ptnGQEjFPvwOLxdVgb4,708
259
260
  astrbot/dashboard/routes/knowledge_base.py,sha256=_3jjcpBjfErP9NI-h4FYBaw3lm5_eIhmfF-uju94Icg,39873
260
261
  astrbot/dashboard/routes/log.py,sha256=84OFiLM-Cnqf3HxFne-ykUezfnArlwH4HyY8MJxch00,2143
261
262
  astrbot/dashboard/routes/persona.py,sha256=MEcNHMxJmyvZ3ZhytI5IP7L3FSlMr1JDvdd5efN9Q-M,7833
262
- astrbot/dashboard/routes/plugin.py,sha256=lc50jRSRcJfpKMrT1OlFDuA7e841SSCEyEhFXiX742c,20508
263
+ astrbot/dashboard/routes/plugin.py,sha256=t0XQdodA5SM7clgUmQAWfE744qlCeOaGUWpDfOrfxZg,22807
263
264
  astrbot/dashboard/routes/route.py,sha256=GT5fYW9fxYmdVj5_6-Wob7nw_-JXuUNDMXGPWKZUbd8,1547
264
265
  astrbot/dashboard/routes/session_management.py,sha256=3h-zlkiAN4MQQLETGORNoWDtnGCSbqxnK2mTu6jMeCY,14998
265
266
  astrbot/dashboard/routes/stat.py,sha256=OgNM491WFuDSAQbbJUGl4_UsqrQNefOGMMRIPLLoVBQ,6780
@@ -267,8 +268,8 @@ astrbot/dashboard/routes/static_file.py,sha256=7KnNcOb1BVqSTft114LhGsDkfg69X2jHE
267
268
  astrbot/dashboard/routes/t2i.py,sha256=F6smxdL99MF7cRw3hqS6-2GErw8Zhsv0V0mfBUeEk-c,8931
268
269
  astrbot/dashboard/routes/tools.py,sha256=YsVFrwVIhxAI-Ikme7YPrHVnPVTkJ1IaH7n6ciREjdE,14663
269
270
  astrbot/dashboard/routes/update.py,sha256=qXiqQ_dbqRVftOzGgCQrvK8-qopVK6zKhhVVJ9SK26U,6648
270
- astrbot-4.7.3.dist-info/METADATA,sha256=bo07BXGbFtONTjCZoigfHCXHHlN0OwJtb_19iZyG6Cc,10370
271
- astrbot-4.7.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
272
- astrbot-4.7.3.dist-info/entry_points.txt,sha256=OEF09YmhBWYuViXrvTLLpstF4ccmNwDL8r7nnFD0pfI,53
273
- astrbot-4.7.3.dist-info/licenses/LICENSE,sha256=zPfQj5Mq8-gThIiBcxETr7t8gND9bZWOjTGQAr80TQI,34500
274
- astrbot-4.7.3.dist-info/RECORD,,
271
+ astrbot-4.7.4.dist-info/METADATA,sha256=iusOE8piaKakYH70-h3ORFCKHzqmmOkImyJpgvHaVIM,10370
272
+ astrbot-4.7.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
273
+ astrbot-4.7.4.dist-info/entry_points.txt,sha256=OEF09YmhBWYuViXrvTLLpstF4ccmNwDL8r7nnFD0pfI,53
274
+ astrbot-4.7.4.dist-info/licenses/LICENSE,sha256=zPfQj5Mq8-gThIiBcxETr7t8gND9bZWOjTGQAr80TQI,34500
275
+ astrbot-4.7.4.dist-info/RECORD,,