AstrBot 4.5.1__py3-none-any.whl → 4.5.2__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/api/__init__.py +10 -11
- astrbot/api/event/__init__.py +5 -6
- astrbot/api/event/filter/__init__.py +37 -36
- astrbot/api/platform/__init__.py +7 -8
- astrbot/api/provider/__init__.py +7 -7
- astrbot/api/star/__init__.py +3 -4
- astrbot/api/util/__init__.py +2 -2
- astrbot/cli/__main__.py +5 -5
- astrbot/cli/commands/__init__.py +3 -3
- astrbot/cli/commands/cmd_conf.py +19 -16
- astrbot/cli/commands/cmd_init.py +3 -2
- astrbot/cli/commands/cmd_plug.py +8 -10
- astrbot/cli/commands/cmd_run.py +5 -6
- astrbot/cli/utils/__init__.py +6 -6
- astrbot/cli/utils/basic.py +14 -14
- astrbot/cli/utils/plugin.py +24 -15
- astrbot/cli/utils/version_comparator.py +10 -12
- astrbot/core/__init__.py +8 -6
- astrbot/core/agent/agent.py +3 -2
- astrbot/core/agent/handoff.py +6 -2
- astrbot/core/agent/hooks.py +9 -6
- astrbot/core/agent/mcp_client.py +50 -15
- astrbot/core/agent/message.py +168 -0
- astrbot/core/agent/response.py +2 -1
- astrbot/core/agent/run_context.py +2 -3
- astrbot/core/agent/runners/base.py +10 -13
- astrbot/core/agent/runners/tool_loop_agent_runner.py +52 -51
- astrbot/core/agent/tool.py +60 -41
- astrbot/core/agent/tool_executor.py +9 -3
- astrbot/core/astr_agent_context.py +3 -1
- astrbot/core/astrbot_config_mgr.py +29 -9
- astrbot/core/config/__init__.py +2 -2
- astrbot/core/config/astrbot_config.py +28 -26
- astrbot/core/config/default.py +4 -6
- astrbot/core/conversation_mgr.py +105 -36
- astrbot/core/core_lifecycle.py +68 -54
- astrbot/core/db/__init__.py +33 -18
- astrbot/core/db/migration/helper.py +12 -10
- astrbot/core/db/migration/migra_3_to_4.py +53 -34
- astrbot/core/db/migration/migra_45_to_46.py +1 -1
- astrbot/core/db/migration/shared_preferences_v3.py +2 -1
- astrbot/core/db/migration/sqlite_v3.py +26 -23
- astrbot/core/db/po.py +27 -18
- astrbot/core/db/sqlite.py +74 -45
- astrbot/core/db/vec_db/base.py +10 -14
- astrbot/core/db/vec_db/faiss_impl/document_storage.py +90 -77
- astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +9 -3
- astrbot/core/db/vec_db/faiss_impl/vec_db.py +36 -31
- astrbot/core/event_bus.py +8 -6
- astrbot/core/file_token_service.py +6 -5
- astrbot/core/initial_loader.py +7 -5
- astrbot/core/knowledge_base/chunking/__init__.py +1 -3
- astrbot/core/knowledge_base/chunking/base.py +1 -0
- astrbot/core/knowledge_base/chunking/fixed_size.py +2 -0
- astrbot/core/knowledge_base/chunking/recursive.py +16 -10
- astrbot/core/knowledge_base/kb_db_sqlite.py +50 -48
- astrbot/core/knowledge_base/kb_helper.py +30 -17
- astrbot/core/knowledge_base/kb_mgr.py +6 -7
- astrbot/core/knowledge_base/models.py +10 -4
- astrbot/core/knowledge_base/parsers/__init__.py +3 -5
- astrbot/core/knowledge_base/parsers/base.py +1 -0
- astrbot/core/knowledge_base/parsers/markitdown_parser.py +2 -1
- astrbot/core/knowledge_base/parsers/pdf_parser.py +2 -1
- astrbot/core/knowledge_base/parsers/text_parser.py +1 -0
- astrbot/core/knowledge_base/parsers/util.py +1 -1
- astrbot/core/knowledge_base/retrieval/__init__.py +6 -8
- astrbot/core/knowledge_base/retrieval/manager.py +17 -14
- astrbot/core/knowledge_base/retrieval/rank_fusion.py +7 -3
- astrbot/core/knowledge_base/retrieval/sparse_retriever.py +11 -5
- astrbot/core/log.py +21 -13
- astrbot/core/message/components.py +123 -217
- astrbot/core/message/message_event_result.py +24 -24
- astrbot/core/persona_mgr.py +20 -11
- astrbot/core/pipeline/__init__.py +7 -7
- astrbot/core/pipeline/content_safety_check/stage.py +13 -9
- astrbot/core/pipeline/content_safety_check/strategies/__init__.py +1 -2
- astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py +12 -13
- astrbot/core/pipeline/content_safety_check/strategies/keywords.py +1 -0
- astrbot/core/pipeline/content_safety_check/strategies/strategy.py +6 -6
- astrbot/core/pipeline/context.py +4 -1
- astrbot/core/pipeline/context_utils.py +77 -7
- astrbot/core/pipeline/preprocess_stage/stage.py +12 -9
- astrbot/core/pipeline/process_stage/method/llm_request.py +125 -72
- astrbot/core/pipeline/process_stage/method/star_request.py +19 -17
- astrbot/core/pipeline/process_stage/stage.py +13 -10
- astrbot/core/pipeline/process_stage/utils.py +6 -5
- astrbot/core/pipeline/rate_limit_check/stage.py +37 -36
- astrbot/core/pipeline/respond/stage.py +23 -20
- astrbot/core/pipeline/result_decorate/stage.py +31 -23
- astrbot/core/pipeline/scheduler.py +12 -8
- astrbot/core/pipeline/session_status_check/stage.py +12 -8
- astrbot/core/pipeline/stage.py +10 -4
- astrbot/core/pipeline/waking_check/stage.py +24 -18
- astrbot/core/pipeline/whitelist_check/stage.py +10 -7
- astrbot/core/platform/__init__.py +6 -6
- astrbot/core/platform/astr_message_event.py +76 -110
- astrbot/core/platform/astrbot_message.py +11 -13
- astrbot/core/platform/manager.py +16 -15
- astrbot/core/platform/message_session.py +5 -3
- astrbot/core/platform/platform.py +16 -24
- astrbot/core/platform/platform_metadata.py +4 -4
- astrbot/core/platform/register.py +8 -8
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +23 -15
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +51 -33
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +42 -27
- astrbot/core/platform/sources/dingtalk/dingtalk_event.py +7 -3
- astrbot/core/platform/sources/discord/client.py +9 -6
- astrbot/core/platform/sources/discord/components.py +18 -14
- astrbot/core/platform/sources/discord/discord_platform_adapter.py +45 -30
- astrbot/core/platform/sources/discord/discord_platform_event.py +38 -30
- astrbot/core/platform/sources/lark/lark_adapter.py +23 -17
- astrbot/core/platform/sources/lark/lark_event.py +21 -14
- astrbot/core/platform/sources/misskey/misskey_adapter.py +107 -67
- astrbot/core/platform/sources/misskey/misskey_api.py +153 -129
- astrbot/core/platform/sources/misskey/misskey_event.py +20 -15
- astrbot/core/platform/sources/misskey/misskey_utils.py +74 -62
- astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +63 -44
- astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +41 -26
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +36 -17
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_event.py +3 -1
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +12 -7
- astrbot/core/platform/sources/satori/satori_adapter.py +56 -38
- astrbot/core/platform/sources/satori/satori_event.py +34 -25
- astrbot/core/platform/sources/slack/client.py +11 -9
- astrbot/core/platform/sources/slack/slack_adapter.py +52 -36
- astrbot/core/platform/sources/slack/slack_event.py +34 -24
- astrbot/core/platform/sources/telegram/tg_adapter.py +38 -18
- astrbot/core/platform/sources/telegram/tg_event.py +32 -18
- astrbot/core/platform/sources/webchat/webchat_adapter.py +27 -17
- astrbot/core/platform/sources/webchat/webchat_event.py +14 -10
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +115 -120
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +9 -8
- astrbot/core/platform/sources/wechatpadpro/xml_data_parser.py +15 -16
- astrbot/core/platform/sources/wecom/wecom_adapter.py +35 -18
- astrbot/core/platform/sources/wecom/wecom_event.py +55 -48
- astrbot/core/platform/sources/wecom/wecom_kf.py +34 -44
- astrbot/core/platform/sources/wecom/wecom_kf_message.py +26 -10
- astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py +18 -10
- astrbot/core/platform/sources/wecom_ai_bot/__init__.py +3 -5
- astrbot/core/platform/sources/wecom_ai_bot/ierror.py +0 -1
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +61 -37
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py +67 -28
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +8 -9
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py +18 -9
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +14 -12
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_utils.py +22 -12
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +40 -26
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +47 -45
- astrbot/core/platform_message_history_mgr.py +5 -3
- astrbot/core/provider/__init__.py +2 -3
- astrbot/core/provider/entites.py +8 -8
- astrbot/core/provider/entities.py +61 -75
- astrbot/core/provider/func_tool_manager.py +59 -55
- astrbot/core/provider/manager.py +32 -22
- astrbot/core/provider/provider.py +72 -46
- astrbot/core/provider/register.py +7 -7
- astrbot/core/provider/sources/anthropic_source.py +48 -30
- astrbot/core/provider/sources/azure_tts_source.py +17 -13
- astrbot/core/provider/sources/coze_api_client.py +27 -17
- astrbot/core/provider/sources/coze_source.py +104 -87
- astrbot/core/provider/sources/dashscope_source.py +18 -11
- astrbot/core/provider/sources/dashscope_tts.py +36 -23
- astrbot/core/provider/sources/dify_source.py +25 -20
- astrbot/core/provider/sources/edge_tts_source.py +21 -17
- astrbot/core/provider/sources/fishaudio_tts_api_source.py +22 -14
- astrbot/core/provider/sources/gemini_embedding_source.py +12 -13
- astrbot/core/provider/sources/gemini_source.py +72 -58
- astrbot/core/provider/sources/gemini_tts_source.py +8 -6
- astrbot/core/provider/sources/gsv_selfhosted_source.py +17 -14
- astrbot/core/provider/sources/gsvi_tts_source.py +11 -7
- astrbot/core/provider/sources/minimax_tts_api_source.py +50 -40
- astrbot/core/provider/sources/openai_embedding_source.py +6 -8
- astrbot/core/provider/sources/openai_source.py +77 -69
- astrbot/core/provider/sources/openai_tts_api_source.py +14 -6
- astrbot/core/provider/sources/sensevoice_selfhosted_source.py +13 -11
- astrbot/core/provider/sources/vllm_rerank_source.py +10 -4
- astrbot/core/provider/sources/volcengine_tts.py +38 -31
- astrbot/core/provider/sources/whisper_api_source.py +14 -12
- astrbot/core/provider/sources/whisper_selfhosted_source.py +15 -11
- astrbot/core/provider/sources/xinference_rerank_source.py +16 -8
- astrbot/core/provider/sources/xinference_stt_provider.py +35 -25
- astrbot/core/star/__init__.py +16 -11
- astrbot/core/star/config.py +10 -15
- astrbot/core/star/context.py +97 -75
- astrbot/core/star/filter/__init__.py +4 -3
- astrbot/core/star/filter/command.py +30 -28
- astrbot/core/star/filter/command_group.py +27 -24
- astrbot/core/star/filter/custom_filter.py +6 -5
- astrbot/core/star/filter/event_message_type.py +4 -2
- astrbot/core/star/filter/permission.py +4 -2
- astrbot/core/star/filter/platform_adapter_type.py +4 -2
- astrbot/core/star/filter/regex.py +4 -2
- astrbot/core/star/register/__init__.py +19 -19
- astrbot/core/star/register/star.py +6 -2
- astrbot/core/star/register/star_handler.py +96 -73
- astrbot/core/star/session_llm_manager.py +48 -14
- astrbot/core/star/session_plugin_manager.py +29 -15
- astrbot/core/star/star.py +1 -2
- astrbot/core/star/star_handler.py +13 -8
- astrbot/core/star/star_manager.py +151 -59
- astrbot/core/star/star_tools.py +44 -37
- astrbot/core/star/updator.py +10 -10
- astrbot/core/umop_config_router.py +10 -4
- astrbot/core/updator.py +13 -5
- astrbot/core/utils/astrbot_path.py +3 -5
- astrbot/core/utils/dify_api_client.py +33 -15
- astrbot/core/utils/io.py +66 -42
- astrbot/core/utils/log_pipe.py +1 -1
- astrbot/core/utils/metrics.py +7 -7
- astrbot/core/utils/path_util.py +15 -16
- astrbot/core/utils/pip_installer.py +5 -5
- astrbot/core/utils/session_waiter.py +19 -20
- astrbot/core/utils/shared_preferences.py +45 -20
- astrbot/core/utils/t2i/__init__.py +4 -1
- astrbot/core/utils/t2i/network_strategy.py +35 -26
- astrbot/core/utils/t2i/renderer.py +11 -5
- astrbot/core/utils/t2i/template_manager.py +14 -15
- astrbot/core/utils/tencent_record_helper.py +19 -13
- astrbot/core/utils/version_comparator.py +10 -13
- astrbot/core/zip_updator.py +43 -40
- astrbot/dashboard/routes/__init__.py +18 -18
- astrbot/dashboard/routes/auth.py +10 -8
- astrbot/dashboard/routes/chat.py +30 -21
- astrbot/dashboard/routes/config.py +92 -75
- astrbot/dashboard/routes/conversation.py +46 -39
- astrbot/dashboard/routes/file.py +4 -2
- astrbot/dashboard/routes/knowledge_base.py +47 -40
- astrbot/dashboard/routes/log.py +9 -4
- astrbot/dashboard/routes/persona.py +19 -16
- astrbot/dashboard/routes/plugin.py +69 -55
- astrbot/dashboard/routes/route.py +3 -1
- astrbot/dashboard/routes/session_management.py +130 -116
- astrbot/dashboard/routes/stat.py +34 -34
- astrbot/dashboard/routes/t2i.py +15 -12
- astrbot/dashboard/routes/tools.py +47 -52
- astrbot/dashboard/routes/update.py +32 -28
- astrbot/dashboard/server.py +30 -26
- astrbot/dashboard/utils.py +8 -4
- {astrbot-4.5.1.dist-info → astrbot-4.5.2.dist-info}/METADATA +2 -1
- astrbot-4.5.2.dist-info/RECORD +261 -0
- astrbot-4.5.1.dist-info/RECORD +0 -260
- {astrbot-4.5.1.dist-info → astrbot-4.5.2.dist-info}/WHEEL +0 -0
- {astrbot-4.5.1.dist-info → astrbot-4.5.2.dist-info}/entry_points.txt +0 -0
- {astrbot-4.5.1.dist-info → astrbot-4.5.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,18 +1,20 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import json
|
|
2
3
|
import random
|
|
3
|
-
import asyncio
|
|
4
|
-
from typing import Any, Optional, Dict, List, Callable, Awaitable
|
|
5
4
|
import uuid
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
from typing import Any
|
|
6
7
|
|
|
7
8
|
try:
|
|
8
9
|
import aiohttp
|
|
9
10
|
import websockets
|
|
10
11
|
except ImportError as e:
|
|
11
12
|
raise ImportError(
|
|
12
|
-
"aiohttp and websockets are required for Misskey API. Please install them with: pip install aiohttp websockets"
|
|
13
|
+
"aiohttp and websockets are required for Misskey API. Please install them with: pip install aiohttp websockets",
|
|
13
14
|
) from e
|
|
14
15
|
|
|
15
16
|
from astrbot.api import logger
|
|
17
|
+
|
|
16
18
|
from .misskey_utils import FileIDExtractor
|
|
17
19
|
|
|
18
20
|
# Constants
|
|
@@ -23,54 +25,47 @@ HTTP_OK = 200
|
|
|
23
25
|
class APIError(Exception):
|
|
24
26
|
"""Misskey API 基础异常"""
|
|
25
27
|
|
|
26
|
-
pass
|
|
27
|
-
|
|
28
28
|
|
|
29
29
|
class APIConnectionError(APIError):
|
|
30
30
|
"""网络连接异常"""
|
|
31
31
|
|
|
32
|
-
pass
|
|
33
|
-
|
|
34
32
|
|
|
35
33
|
class APIRateLimitError(APIError):
|
|
36
34
|
"""API 频率限制异常"""
|
|
37
35
|
|
|
38
|
-
pass
|
|
39
|
-
|
|
40
36
|
|
|
41
37
|
class AuthenticationError(APIError):
|
|
42
38
|
"""认证失败异常"""
|
|
43
39
|
|
|
44
|
-
pass
|
|
45
|
-
|
|
46
40
|
|
|
47
41
|
class WebSocketError(APIError):
|
|
48
42
|
"""WebSocket 连接异常"""
|
|
49
43
|
|
|
50
|
-
pass
|
|
51
|
-
|
|
52
44
|
|
|
53
45
|
class StreamingClient:
|
|
54
46
|
def __init__(self, instance_url: str, access_token: str):
|
|
55
47
|
self.instance_url = instance_url.rstrip("/")
|
|
56
48
|
self.access_token = access_token
|
|
57
|
-
self.websocket:
|
|
49
|
+
self.websocket: Any | None = None
|
|
58
50
|
self.is_connected = False
|
|
59
|
-
self.message_handlers:
|
|
60
|
-
self.channels:
|
|
61
|
-
self.desired_channels:
|
|
51
|
+
self.message_handlers: dict[str, Callable] = {}
|
|
52
|
+
self.channels: dict[str, str] = {}
|
|
53
|
+
self.desired_channels: dict[str, dict | None] = {}
|
|
62
54
|
self._running = False
|
|
63
55
|
self._last_pong = None
|
|
64
56
|
|
|
65
57
|
async def connect(self) -> bool:
|
|
66
58
|
try:
|
|
67
59
|
ws_url = self.instance_url.replace("https://", "wss://").replace(
|
|
68
|
-
"http://",
|
|
60
|
+
"http://",
|
|
61
|
+
"ws://",
|
|
69
62
|
)
|
|
70
63
|
ws_url += f"/streaming?i={self.access_token}"
|
|
71
64
|
|
|
72
65
|
self.websocket = await websockets.connect(
|
|
73
|
-
ws_url,
|
|
66
|
+
ws_url,
|
|
67
|
+
ping_interval=30,
|
|
68
|
+
ping_timeout=10,
|
|
74
69
|
)
|
|
75
70
|
self.is_connected = True
|
|
76
71
|
self._running = True
|
|
@@ -84,7 +79,7 @@ class StreamingClient:
|
|
|
84
79
|
await self.subscribe_channel(channel_type, params)
|
|
85
80
|
except Exception as e:
|
|
86
81
|
logger.warning(
|
|
87
|
-
f"[Misskey WebSocket] 重新订阅 {channel_type} 失败: {e}"
|
|
82
|
+
f"[Misskey WebSocket] 重新订阅 {channel_type} 失败: {e}",
|
|
88
83
|
)
|
|
89
84
|
except Exception:
|
|
90
85
|
pass
|
|
@@ -104,7 +99,9 @@ class StreamingClient:
|
|
|
104
99
|
logger.info("[Misskey WebSocket] 连接已断开")
|
|
105
100
|
|
|
106
101
|
async def subscribe_channel(
|
|
107
|
-
self,
|
|
102
|
+
self,
|
|
103
|
+
channel_type: str,
|
|
104
|
+
params: dict | None = None,
|
|
108
105
|
) -> str:
|
|
109
106
|
if not self.is_connected or not self.websocket:
|
|
110
107
|
raise WebSocketError("WebSocket 未连接")
|
|
@@ -136,7 +133,9 @@ class StreamingClient:
|
|
|
136
133
|
self.desired_channels.pop(channel_type, None)
|
|
137
134
|
|
|
138
135
|
def add_message_handler(
|
|
139
|
-
self,
|
|
136
|
+
self,
|
|
137
|
+
event_type: str,
|
|
138
|
+
handler: Callable[[dict], Awaitable[None]],
|
|
140
139
|
):
|
|
141
140
|
self.message_handlers[event_type] = handler
|
|
142
141
|
|
|
@@ -166,7 +165,7 @@ class StreamingClient:
|
|
|
166
165
|
pass
|
|
167
166
|
except websockets.exceptions.ConnectionClosed as e:
|
|
168
167
|
logger.warning(
|
|
169
|
-
f"[Misskey WebSocket] 连接已关闭 (代码: {e.code}, 原因: {e.reason})"
|
|
168
|
+
f"[Misskey WebSocket] 连接已关闭 (代码: {e.code}, 原因: {e.reason})",
|
|
170
169
|
)
|
|
171
170
|
self.is_connected = False
|
|
172
171
|
try:
|
|
@@ -188,11 +187,11 @@ class StreamingClient:
|
|
|
188
187
|
except Exception:
|
|
189
188
|
pass
|
|
190
189
|
|
|
191
|
-
async def _handle_message(self, data:
|
|
190
|
+
async def _handle_message(self, data: dict[str, Any]):
|
|
192
191
|
message_type = data.get("type")
|
|
193
192
|
body = data.get("body", {})
|
|
194
193
|
|
|
195
|
-
def _build_channel_summary(message_type:
|
|
194
|
+
def _build_channel_summary(message_type: str | None, body: Any) -> str:
|
|
196
195
|
try:
|
|
197
196
|
if not isinstance(body, dict):
|
|
198
197
|
return f"[Misskey WebSocket] 收到消息类型: {message_type}"
|
|
@@ -228,7 +227,7 @@ class StreamingClient:
|
|
|
228
227
|
event_body = body.get("body", {})
|
|
229
228
|
|
|
230
229
|
logger.debug(
|
|
231
|
-
f"[Misskey WebSocket] 频道消息: {channel_id}, 事件类型: {event_type}"
|
|
230
|
+
f"[Misskey WebSocket] 频道消息: {channel_id}, 事件类型: {event_type}",
|
|
232
231
|
)
|
|
233
232
|
|
|
234
233
|
if channel_id in self.channels:
|
|
@@ -243,7 +242,7 @@ class StreamingClient:
|
|
|
243
242
|
await self.message_handlers[event_type](event_body)
|
|
244
243
|
else:
|
|
245
244
|
logger.debug(
|
|
246
|
-
f"[Misskey WebSocket] 未找到处理器: {handler_key} 或 {event_type}"
|
|
245
|
+
f"[Misskey WebSocket] 未找到处理器: {handler_key} 或 {event_type}",
|
|
247
246
|
)
|
|
248
247
|
if "_debug" in self.message_handlers:
|
|
249
248
|
await self.message_handlers["_debug"](
|
|
@@ -251,7 +250,7 @@ class StreamingClient:
|
|
|
251
250
|
"type": event_type,
|
|
252
251
|
"body": event_body,
|
|
253
252
|
"channel": channel_type,
|
|
254
|
-
}
|
|
253
|
+
},
|
|
255
254
|
)
|
|
256
255
|
|
|
257
256
|
elif message_type in self.message_handlers:
|
|
@@ -269,14 +268,14 @@ def retry_async(
|
|
|
269
268
|
backoff_base: float = 1.0,
|
|
270
269
|
max_backoff: float = 30.0,
|
|
271
270
|
):
|
|
272
|
-
"""
|
|
273
|
-
智能异步重试装饰器
|
|
271
|
+
"""智能异步重试装饰器
|
|
274
272
|
|
|
275
273
|
Args:
|
|
276
274
|
max_retries: 最大重试次数
|
|
277
275
|
retryable_exceptions: 可重试的异常类型
|
|
278
276
|
backoff_base: 退避基数
|
|
279
277
|
max_backoff: 最大退避时间
|
|
278
|
+
|
|
280
279
|
"""
|
|
281
280
|
|
|
282
281
|
def decorator(func):
|
|
@@ -291,7 +290,7 @@ def retry_async(
|
|
|
291
290
|
last_exc = e
|
|
292
291
|
if attempt == max_retries:
|
|
293
292
|
logger.error(
|
|
294
|
-
f"[Misskey API] {func_name} 重试 {max_retries} 次后仍失败: {e}"
|
|
293
|
+
f"[Misskey API] {func_name} 重试 {max_retries} 次后仍失败: {e}",
|
|
295
294
|
)
|
|
296
295
|
break
|
|
297
296
|
|
|
@@ -308,7 +307,7 @@ def retry_async(
|
|
|
308
307
|
|
|
309
308
|
logger.warning(
|
|
310
309
|
f"[Misskey API] {func_name} 第 {attempt} 次重试失败: {e},"
|
|
311
|
-
f"{sleep_time:.1f}s后重试"
|
|
310
|
+
f"{sleep_time:.1f}s后重试",
|
|
312
311
|
)
|
|
313
312
|
await asyncio.sleep(sleep_time)
|
|
314
313
|
continue
|
|
@@ -334,12 +333,12 @@ class MisskeyAPI:
|
|
|
334
333
|
allow_insecure_downloads: bool = False,
|
|
335
334
|
download_timeout: int = 15,
|
|
336
335
|
chunk_size: int = 64 * 1024,
|
|
337
|
-
max_download_bytes:
|
|
336
|
+
max_download_bytes: int | None = None,
|
|
338
337
|
):
|
|
339
338
|
self.instance_url = instance_url.rstrip("/")
|
|
340
339
|
self.access_token = access_token
|
|
341
|
-
self._session:
|
|
342
|
-
self.streaming:
|
|
340
|
+
self._session: aiohttp.ClientSession | None = None
|
|
341
|
+
self.streaming: StreamingClient | None = None
|
|
343
342
|
# download options
|
|
344
343
|
self.allow_insecure_downloads = allow_insecure_downloads
|
|
345
344
|
self.download_timeout = download_timeout
|
|
@@ -381,39 +380,40 @@ class MisskeyAPI:
|
|
|
381
380
|
if status == 400:
|
|
382
381
|
logger.error(f"[Misskey API] 请求参数错误: {endpoint} (HTTP {status})")
|
|
383
382
|
raise APIError(f"Bad request for {endpoint}")
|
|
384
|
-
|
|
383
|
+
if status == 401:
|
|
385
384
|
logger.error(f"[Misskey API] 未授权访问: {endpoint} (HTTP {status})")
|
|
386
385
|
raise AuthenticationError(f"Unauthorized access for {endpoint}")
|
|
387
|
-
|
|
386
|
+
if status == 403:
|
|
388
387
|
logger.error(f"[Misskey API] 访问被禁止: {endpoint} (HTTP {status})")
|
|
389
388
|
raise AuthenticationError(f"Forbidden access for {endpoint}")
|
|
390
|
-
|
|
389
|
+
if status == 404:
|
|
391
390
|
logger.error(f"[Misskey API] 资源不存在: {endpoint} (HTTP {status})")
|
|
392
391
|
raise APIError(f"Resource not found for {endpoint}")
|
|
393
|
-
|
|
392
|
+
if status == 413:
|
|
394
393
|
logger.error(f"[Misskey API] 请求体过大: {endpoint} (HTTP {status})")
|
|
395
394
|
raise APIError(f"Request entity too large for {endpoint}")
|
|
396
|
-
|
|
395
|
+
if status == 429:
|
|
397
396
|
logger.warning(f"[Misskey API] 请求频率限制: {endpoint} (HTTP {status})")
|
|
398
397
|
raise APIRateLimitError(f"Rate limit exceeded for {endpoint}")
|
|
399
|
-
|
|
398
|
+
if status == 500:
|
|
400
399
|
logger.error(f"[Misskey API] 服务器内部错误: {endpoint} (HTTP {status})")
|
|
401
400
|
raise APIConnectionError(f"Internal server error for {endpoint}")
|
|
402
|
-
|
|
401
|
+
if status == 502:
|
|
403
402
|
logger.error(f"[Misskey API] 网关错误: {endpoint} (HTTP {status})")
|
|
404
403
|
raise APIConnectionError(f"Bad gateway for {endpoint}")
|
|
405
|
-
|
|
404
|
+
if status == 503:
|
|
406
405
|
logger.error(f"[Misskey API] 服务不可用: {endpoint} (HTTP {status})")
|
|
407
406
|
raise APIConnectionError(f"Service unavailable for {endpoint}")
|
|
408
|
-
|
|
407
|
+
if status == 504:
|
|
409
408
|
logger.error(f"[Misskey API] 网关超时: {endpoint} (HTTP {status})")
|
|
410
409
|
raise APIConnectionError(f"Gateway timeout for {endpoint}")
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
raise APIConnectionError(f"HTTP {status} for {endpoint}")
|
|
410
|
+
logger.error(f"[Misskey API] 未知错误: {endpoint} (HTTP {status})")
|
|
411
|
+
raise APIConnectionError(f"HTTP {status} for {endpoint}")
|
|
414
412
|
|
|
415
413
|
async def _process_response(
|
|
416
|
-
self,
|
|
414
|
+
self,
|
|
415
|
+
response: aiohttp.ClientResponse,
|
|
416
|
+
endpoint: str,
|
|
417
417
|
) -> Any:
|
|
418
418
|
"""处理 API 响应"""
|
|
419
419
|
if response.status == HTTP_OK:
|
|
@@ -429,7 +429,7 @@ class MisskeyAPI:
|
|
|
429
429
|
)
|
|
430
430
|
if notifications_data:
|
|
431
431
|
logger.debug(
|
|
432
|
-
f"[Misskey API] 获取到 {len(notifications_data)} 条新通知"
|
|
432
|
+
f"[Misskey API] 获取到 {len(notifications_data)} 条新通知",
|
|
433
433
|
)
|
|
434
434
|
else:
|
|
435
435
|
logger.debug(f"[Misskey API] 请求成功: {endpoint}")
|
|
@@ -441,11 +441,11 @@ class MisskeyAPI:
|
|
|
441
441
|
try:
|
|
442
442
|
error_text = await response.text()
|
|
443
443
|
logger.error(
|
|
444
|
-
f"[Misskey API] 请求失败: {endpoint} - HTTP {response.status}, 响应: {error_text}"
|
|
444
|
+
f"[Misskey API] 请求失败: {endpoint} - HTTP {response.status}, 响应: {error_text}",
|
|
445
445
|
)
|
|
446
446
|
except Exception:
|
|
447
447
|
logger.error(
|
|
448
|
-
f"[Misskey API] 请求失败: {endpoint} - HTTP {response.status}"
|
|
448
|
+
f"[Misskey API] 请求失败: {endpoint} - HTTP {response.status}",
|
|
449
449
|
)
|
|
450
450
|
|
|
451
451
|
self._handle_response_status(response.status, endpoint)
|
|
@@ -456,7 +456,9 @@ class MisskeyAPI:
|
|
|
456
456
|
retryable_exceptions=(APIConnectionError, APIRateLimitError),
|
|
457
457
|
)
|
|
458
458
|
async def _make_request(
|
|
459
|
-
self,
|
|
459
|
+
self,
|
|
460
|
+
endpoint: str,
|
|
461
|
+
data: dict[str, Any] | None = None,
|
|
460
462
|
) -> Any:
|
|
461
463
|
url = f"{self.instance_url}/api/{endpoint}"
|
|
462
464
|
payload = {"i": self.access_token}
|
|
@@ -472,24 +474,24 @@ class MisskeyAPI:
|
|
|
472
474
|
|
|
473
475
|
async def create_note(
|
|
474
476
|
self,
|
|
475
|
-
text:
|
|
477
|
+
text: str | None = None,
|
|
476
478
|
visibility: str = "public",
|
|
477
|
-
reply_id:
|
|
478
|
-
visible_user_ids:
|
|
479
|
-
file_ids:
|
|
479
|
+
reply_id: str | None = None,
|
|
480
|
+
visible_user_ids: list[str] | None = None,
|
|
481
|
+
file_ids: list[str] | None = None,
|
|
480
482
|
local_only: bool = False,
|
|
481
|
-
cw:
|
|
482
|
-
poll:
|
|
483
|
-
renote_id:
|
|
484
|
-
channel_id:
|
|
485
|
-
reaction_acceptance:
|
|
486
|
-
no_extract_mentions:
|
|
487
|
-
no_extract_hashtags:
|
|
488
|
-
no_extract_emojis:
|
|
489
|
-
media_ids:
|
|
490
|
-
) ->
|
|
483
|
+
cw: str | None = None,
|
|
484
|
+
poll: dict[str, Any] | None = None,
|
|
485
|
+
renote_id: str | None = None,
|
|
486
|
+
channel_id: str | None = None,
|
|
487
|
+
reaction_acceptance: str | None = None,
|
|
488
|
+
no_extract_mentions: bool | None = None,
|
|
489
|
+
no_extract_hashtags: bool | None = None,
|
|
490
|
+
no_extract_emojis: bool | None = None,
|
|
491
|
+
media_ids: list[str] | None = None,
|
|
492
|
+
) -> dict[str, Any]:
|
|
491
493
|
"""Create a note (wrapper for notes/create). All additional fields are optional and passed through to the API."""
|
|
492
|
-
data:
|
|
494
|
+
data: dict[str, Any] = {}
|
|
493
495
|
|
|
494
496
|
if text is not None:
|
|
495
497
|
data["text"] = text
|
|
@@ -537,9 +539,9 @@ class MisskeyAPI:
|
|
|
537
539
|
async def upload_file(
|
|
538
540
|
self,
|
|
539
541
|
file_path: str,
|
|
540
|
-
name:
|
|
541
|
-
folder_id:
|
|
542
|
-
) ->
|
|
542
|
+
name: str | None = None,
|
|
543
|
+
folder_id: str | None = None,
|
|
544
|
+
) -> dict[str, Any]:
|
|
543
545
|
"""Upload a file to Misskey drive/files/create and return a dict containing id and raw result."""
|
|
544
546
|
if not file_path:
|
|
545
547
|
raise APIError("No file path provided for upload")
|
|
@@ -565,7 +567,7 @@ class MisskeyAPI:
|
|
|
565
567
|
result = await self._process_response(resp, "drive/files/create")
|
|
566
568
|
file_id = FileIDExtractor.extract_file_id(result)
|
|
567
569
|
logger.debug(
|
|
568
|
-
f"[Misskey API] 本地文件上传成功: {filename} -> {file_id}"
|
|
570
|
+
f"[Misskey API] 本地文件上传成功: {filename} -> {file_id}",
|
|
569
571
|
)
|
|
570
572
|
return {"id": file_id, "raw": result}
|
|
571
573
|
finally:
|
|
@@ -574,7 +576,7 @@ class MisskeyAPI:
|
|
|
574
576
|
logger.error(f"[Misskey API] 文件上传网络错误: {e}")
|
|
575
577
|
raise APIConnectionError(f"Upload failed: {e}") from e
|
|
576
578
|
|
|
577
|
-
async def find_files_by_hash(self, md5_hash: str) ->
|
|
579
|
+
async def find_files_by_hash(self, md5_hash: str) -> list[dict[str, Any]]:
|
|
578
580
|
"""Find files by MD5 hash"""
|
|
579
581
|
if not md5_hash:
|
|
580
582
|
raise APIError("No MD5 hash provided for find-by-hash")
|
|
@@ -585,7 +587,7 @@ class MisskeyAPI:
|
|
|
585
587
|
logger.debug(f"[Misskey API] find-by-hash 请求: md5={md5_hash}")
|
|
586
588
|
result = await self._make_request("drive/files/find-by-hash", data)
|
|
587
589
|
logger.debug(
|
|
588
|
-
f"[Misskey API] find-by-hash 响应: 找到 {len(result) if isinstance(result, list) else 0} 个文件"
|
|
590
|
+
f"[Misskey API] find-by-hash 响应: 找到 {len(result) if isinstance(result, list) else 0} 个文件",
|
|
589
591
|
)
|
|
590
592
|
return result if isinstance(result, list) else []
|
|
591
593
|
except Exception as e:
|
|
@@ -593,13 +595,15 @@ class MisskeyAPI:
|
|
|
593
595
|
raise
|
|
594
596
|
|
|
595
597
|
async def find_files_by_name(
|
|
596
|
-
self,
|
|
597
|
-
|
|
598
|
+
self,
|
|
599
|
+
name: str,
|
|
600
|
+
folder_id: str | None = None,
|
|
601
|
+
) -> list[dict[str, Any]]:
|
|
598
602
|
"""Find files by name"""
|
|
599
603
|
if not name:
|
|
600
604
|
raise APIError("No name provided for find")
|
|
601
605
|
|
|
602
|
-
data:
|
|
606
|
+
data: dict[str, Any] = {"name": name}
|
|
603
607
|
if folder_id:
|
|
604
608
|
data["folderId"] = folder_id
|
|
605
609
|
|
|
@@ -607,7 +611,7 @@ class MisskeyAPI:
|
|
|
607
611
|
logger.debug(f"[Misskey API] find 请求: name={name}, folder_id={folder_id}")
|
|
608
612
|
result = await self._make_request("drive/files/find", data)
|
|
609
613
|
logger.debug(
|
|
610
|
-
f"[Misskey API] find 响应: 找到 {len(result) if isinstance(result, list) else 0} 个文件"
|
|
614
|
+
f"[Misskey API] find 响应: 找到 {len(result) if isinstance(result, list) else 0} 个文件",
|
|
611
615
|
)
|
|
612
616
|
return result if isinstance(result, list) else []
|
|
613
617
|
except Exception as e:
|
|
@@ -617,11 +621,11 @@ class MisskeyAPI:
|
|
|
617
621
|
async def find_files(
|
|
618
622
|
self,
|
|
619
623
|
limit: int = 10,
|
|
620
|
-
folder_id:
|
|
621
|
-
type:
|
|
622
|
-
) ->
|
|
624
|
+
folder_id: str | None = None,
|
|
625
|
+
type: str | None = None,
|
|
626
|
+
) -> list[dict[str, Any]]:
|
|
623
627
|
"""List files with optional filters"""
|
|
624
|
-
data:
|
|
628
|
+
data: dict[str, Any] = {"limit": limit}
|
|
625
629
|
if folder_id is not None:
|
|
626
630
|
data["folderId"] = folder_id
|
|
627
631
|
if type is not None:
|
|
@@ -629,11 +633,11 @@ class MisskeyAPI:
|
|
|
629
633
|
|
|
630
634
|
try:
|
|
631
635
|
logger.debug(
|
|
632
|
-
f"[Misskey API] 列表文件请求: limit={limit}, folder_id={folder_id}, type={type}"
|
|
636
|
+
f"[Misskey API] 列表文件请求: limit={limit}, folder_id={folder_id}, type={type}",
|
|
633
637
|
)
|
|
634
638
|
result = await self._make_request("drive/files", data)
|
|
635
639
|
logger.debug(
|
|
636
|
-
f"[Misskey API] 列表文件响应: 找到 {len(result) if isinstance(result, list) else 0} 个文件"
|
|
640
|
+
f"[Misskey API] 列表文件响应: 找到 {len(result) if isinstance(result, list) else 0} 个文件",
|
|
637
641
|
)
|
|
638
642
|
return result if isinstance(result, list) else []
|
|
639
643
|
except Exception as e:
|
|
@@ -641,27 +645,34 @@ class MisskeyAPI:
|
|
|
641
645
|
raise
|
|
642
646
|
|
|
643
647
|
async def _download_with_existing_session(
|
|
644
|
-
self,
|
|
645
|
-
|
|
648
|
+
self,
|
|
649
|
+
url: str,
|
|
650
|
+
ssl_verify: bool = True,
|
|
651
|
+
) -> bytes | None:
|
|
646
652
|
"""使用现有会话下载文件"""
|
|
647
653
|
if not (hasattr(self, "session") and self.session):
|
|
648
654
|
raise APIConnectionError("No existing session available")
|
|
649
655
|
|
|
650
656
|
async with self.session.get(
|
|
651
|
-
url,
|
|
657
|
+
url,
|
|
658
|
+
timeout=aiohttp.ClientTimeout(total=15),
|
|
659
|
+
ssl=ssl_verify,
|
|
652
660
|
) as response:
|
|
653
661
|
if response.status == 200:
|
|
654
662
|
return await response.read()
|
|
655
663
|
return None
|
|
656
664
|
|
|
657
665
|
async def _download_with_temp_session(
|
|
658
|
-
self,
|
|
659
|
-
|
|
666
|
+
self,
|
|
667
|
+
url: str,
|
|
668
|
+
ssl_verify: bool = True,
|
|
669
|
+
) -> bytes | None:
|
|
660
670
|
"""使用临时会话下载文件"""
|
|
661
671
|
connector = aiohttp.TCPConnector(ssl=ssl_verify)
|
|
662
672
|
async with aiohttp.ClientSession(connector=connector) as temp_session:
|
|
663
673
|
async with temp_session.get(
|
|
664
|
-
url,
|
|
674
|
+
url,
|
|
675
|
+
timeout=aiohttp.ClientTimeout(total=15),
|
|
665
676
|
) as response:
|
|
666
677
|
if response.status == 200:
|
|
667
678
|
return await response.read()
|
|
@@ -670,13 +681,12 @@ class MisskeyAPI:
|
|
|
670
681
|
async def upload_and_find_file(
|
|
671
682
|
self,
|
|
672
683
|
url: str,
|
|
673
|
-
name:
|
|
674
|
-
folder_id:
|
|
684
|
+
name: str | None = None,
|
|
685
|
+
folder_id: str | None = None,
|
|
675
686
|
max_wait_time: float = 30.0,
|
|
676
687
|
check_interval: float = 2.0,
|
|
677
|
-
) ->
|
|
678
|
-
"""
|
|
679
|
-
简化的文件上传:尝试 URL 上传,失败则下载后本地上传
|
|
688
|
+
) -> dict[str, Any] | None:
|
|
689
|
+
"""简化的文件上传:尝试 URL 上传,失败则下载后本地上传
|
|
680
690
|
|
|
681
691
|
Args:
|
|
682
692
|
url: 文件URL
|
|
@@ -687,28 +697,31 @@ class MisskeyAPI:
|
|
|
687
697
|
|
|
688
698
|
Returns:
|
|
689
699
|
包含文件ID和元信息的字典,失败时返回None
|
|
700
|
+
|
|
690
701
|
"""
|
|
691
702
|
if not url:
|
|
692
703
|
raise APIError("URL不能为空")
|
|
693
704
|
|
|
694
705
|
# 通过本地上传获取即时文件 ID(下载文件 → 上传 → 返回 ID)
|
|
695
706
|
try:
|
|
696
|
-
import tempfile
|
|
697
707
|
import os
|
|
708
|
+
import tempfile
|
|
698
709
|
|
|
699
710
|
# SSL 验证下载,失败则重试不验证 SSL
|
|
700
711
|
tmp_bytes = None
|
|
701
712
|
try:
|
|
702
713
|
tmp_bytes = await self._download_with_existing_session(
|
|
703
|
-
url,
|
|
714
|
+
url,
|
|
715
|
+
ssl_verify=True,
|
|
704
716
|
) or await self._download_with_temp_session(url, ssl_verify=True)
|
|
705
717
|
except Exception as ssl_error:
|
|
706
718
|
logger.debug(
|
|
707
|
-
f"[Misskey API] SSL 验证下载失败: {ssl_error},重试不验证 SSL"
|
|
719
|
+
f"[Misskey API] SSL 验证下载失败: {ssl_error},重试不验证 SSL",
|
|
708
720
|
)
|
|
709
721
|
try:
|
|
710
722
|
tmp_bytes = await self._download_with_existing_session(
|
|
711
|
-
url,
|
|
723
|
+
url,
|
|
724
|
+
ssl_verify=False,
|
|
712
725
|
) or await self._download_with_temp_session(url, ssl_verify=False)
|
|
713
726
|
except Exception:
|
|
714
727
|
pass
|
|
@@ -732,13 +745,15 @@ class MisskeyAPI:
|
|
|
732
745
|
|
|
733
746
|
return None
|
|
734
747
|
|
|
735
|
-
async def get_current_user(self) ->
|
|
748
|
+
async def get_current_user(self) -> dict[str, Any]:
|
|
736
749
|
"""获取当前用户信息"""
|
|
737
750
|
return await self._make_request("i", {})
|
|
738
751
|
|
|
739
752
|
async def send_message(
|
|
740
|
-
self,
|
|
741
|
-
|
|
753
|
+
self,
|
|
754
|
+
user_id_or_payload: Any,
|
|
755
|
+
text: str | None = None,
|
|
756
|
+
) -> dict[str, Any]:
|
|
742
757
|
"""发送聊天消息。
|
|
743
758
|
|
|
744
759
|
Accepts either (user_id: str, text: str) or a single dict payload prepared by caller.
|
|
@@ -754,8 +769,10 @@ class MisskeyAPI:
|
|
|
754
769
|
return result
|
|
755
770
|
|
|
756
771
|
async def send_room_message(
|
|
757
|
-
self,
|
|
758
|
-
|
|
772
|
+
self,
|
|
773
|
+
room_id_or_payload: Any,
|
|
774
|
+
text: str | None = None,
|
|
775
|
+
) -> dict[str, Any]:
|
|
759
776
|
"""发送房间消息。
|
|
760
777
|
|
|
761
778
|
Accepts either (room_id: str, text: str) or a single dict payload.
|
|
@@ -771,10 +788,13 @@ class MisskeyAPI:
|
|
|
771
788
|
return result
|
|
772
789
|
|
|
773
790
|
async def get_messages(
|
|
774
|
-
self,
|
|
775
|
-
|
|
791
|
+
self,
|
|
792
|
+
user_id: str,
|
|
793
|
+
limit: int = 10,
|
|
794
|
+
since_id: str | None = None,
|
|
795
|
+
) -> list[dict[str, Any]]:
|
|
776
796
|
"""获取聊天消息历史"""
|
|
777
|
-
data:
|
|
797
|
+
data: dict[str, Any] = {"userId": user_id, "limit": limit}
|
|
778
798
|
if since_id:
|
|
779
799
|
data["sinceId"] = since_id
|
|
780
800
|
|
|
@@ -785,10 +805,12 @@ class MisskeyAPI:
|
|
|
785
805
|
return []
|
|
786
806
|
|
|
787
807
|
async def get_mentions(
|
|
788
|
-
self,
|
|
789
|
-
|
|
808
|
+
self,
|
|
809
|
+
limit: int = 10,
|
|
810
|
+
since_id: str | None = None,
|
|
811
|
+
) -> list[dict[str, Any]]:
|
|
790
812
|
"""获取提及通知"""
|
|
791
|
-
data:
|
|
813
|
+
data: dict[str, Any] = {"limit": limit}
|
|
792
814
|
if since_id:
|
|
793
815
|
data["sinceId"] = since_id
|
|
794
816
|
data["includeTypes"] = ["mention", "reply", "quote"]
|
|
@@ -796,23 +818,21 @@ class MisskeyAPI:
|
|
|
796
818
|
result = await self._make_request("i/notifications", data)
|
|
797
819
|
if isinstance(result, list):
|
|
798
820
|
return result
|
|
799
|
-
|
|
821
|
+
if isinstance(result, dict) and "notifications" in result:
|
|
800
822
|
return result["notifications"]
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
return []
|
|
823
|
+
logger.warning(f"[Misskey API] 提及通知响应格式异常: {type(result)}")
|
|
824
|
+
return []
|
|
804
825
|
|
|
805
826
|
async def send_message_with_media(
|
|
806
827
|
self,
|
|
807
828
|
message_type: str,
|
|
808
829
|
target_id: str,
|
|
809
|
-
text:
|
|
810
|
-
media_urls:
|
|
811
|
-
local_files:
|
|
830
|
+
text: str | None = None,
|
|
831
|
+
media_urls: list[str] | None = None,
|
|
832
|
+
local_files: list[str] | None = None,
|
|
812
833
|
**kwargs,
|
|
813
|
-
) ->
|
|
814
|
-
"""
|
|
815
|
-
通用消息发送函数:统一处理文本+媒体发送
|
|
834
|
+
) -> dict[str, Any]:
|
|
835
|
+
"""通用消息发送函数:统一处理文本+媒体发送
|
|
816
836
|
|
|
817
837
|
Args:
|
|
818
838
|
message_type: 消息类型 ('chat', 'room', 'note')
|
|
@@ -827,6 +847,7 @@ class MisskeyAPI:
|
|
|
827
847
|
|
|
828
848
|
Raises:
|
|
829
849
|
APIError: 参数错误或发送失败
|
|
850
|
+
|
|
830
851
|
"""
|
|
831
852
|
if not text and not media_urls and not local_files:
|
|
832
853
|
raise APIError("消息内容不能为空:需要文本或媒体文件")
|
|
@@ -843,10 +864,14 @@ class MisskeyAPI:
|
|
|
843
864
|
|
|
844
865
|
# 根据消息类型发送
|
|
845
866
|
return await self._dispatch_message(
|
|
846
|
-
message_type,
|
|
867
|
+
message_type,
|
|
868
|
+
target_id,
|
|
869
|
+
text,
|
|
870
|
+
file_ids,
|
|
871
|
+
**kwargs,
|
|
847
872
|
)
|
|
848
873
|
|
|
849
|
-
async def _process_media_urls(self, urls:
|
|
874
|
+
async def _process_media_urls(self, urls: list[str]) -> list[str]:
|
|
850
875
|
"""处理远程媒体文件URL列表,返回文件ID列表"""
|
|
851
876
|
file_ids = []
|
|
852
877
|
for url in urls:
|
|
@@ -863,7 +888,7 @@ class MisskeyAPI:
|
|
|
863
888
|
continue
|
|
864
889
|
return file_ids
|
|
865
890
|
|
|
866
|
-
async def _process_local_files(self, file_paths:
|
|
891
|
+
async def _process_local_files(self, file_paths: list[str]) -> list[str]:
|
|
867
892
|
"""处理本地文件路径列表,返回文件ID列表"""
|
|
868
893
|
file_ids = []
|
|
869
894
|
for file_path in file_paths:
|
|
@@ -883,10 +908,10 @@ class MisskeyAPI:
|
|
|
883
908
|
self,
|
|
884
909
|
message_type: str,
|
|
885
910
|
target_id: str,
|
|
886
|
-
text:
|
|
887
|
-
file_ids:
|
|
911
|
+
text: str | None,
|
|
912
|
+
file_ids: list[str],
|
|
888
913
|
**kwargs,
|
|
889
|
-
) ->
|
|
914
|
+
) -> dict[str, Any]:
|
|
890
915
|
"""根据消息类型分发到对应的发送方法"""
|
|
891
916
|
if message_type == "chat":
|
|
892
917
|
# 聊天消息使用 fileId (单数)
|
|
@@ -907,7 +932,7 @@ class MisskeyAPI:
|
|
|
907
932
|
return {"multiple": True, "results": results}
|
|
908
933
|
return await self.send_message(payload)
|
|
909
934
|
|
|
910
|
-
|
|
935
|
+
if message_type == "room":
|
|
911
936
|
# 房间消息使用 fileId (单数)
|
|
912
937
|
payload = {"toRoomId": target_id}
|
|
913
938
|
if text:
|
|
@@ -926,7 +951,7 @@ class MisskeyAPI:
|
|
|
926
951
|
return {"multiple": True, "results": results}
|
|
927
952
|
return await self.send_room_message(payload)
|
|
928
953
|
|
|
929
|
-
|
|
954
|
+
if message_type == "note":
|
|
930
955
|
# 发帖使用 fileIds (复数)
|
|
931
956
|
note_kwargs = {
|
|
932
957
|
"text": text,
|
|
@@ -936,5 +961,4 @@ class MisskeyAPI:
|
|
|
936
961
|
note_kwargs.update(kwargs)
|
|
937
962
|
return await self.create_note(**note_kwargs)
|
|
938
963
|
|
|
939
|
-
|
|
940
|
-
raise APIError(f"不支持的消息类型: {message_type}")
|
|
964
|
+
raise APIError(f"不支持的消息类型: {message_type}")
|