AstrBot 4.3.5__py3-none-any.whl → 4.5.1__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/core/agent/runners/tool_loop_agent_runner.py +31 -2
- astrbot/core/astrbot_config_mgr.py +23 -51
- astrbot/core/config/default.py +132 -12
- astrbot/core/conversation_mgr.py +36 -1
- astrbot/core/core_lifecycle.py +24 -5
- astrbot/core/db/migration/helper.py +6 -3
- astrbot/core/db/migration/migra_45_to_46.py +44 -0
- astrbot/core/db/vec_db/base.py +33 -2
- astrbot/core/db/vec_db/faiss_impl/document_storage.py +310 -52
- astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +31 -3
- astrbot/core/db/vec_db/faiss_impl/vec_db.py +81 -23
- astrbot/core/file_token_service.py +6 -1
- astrbot/core/initial_loader.py +6 -3
- astrbot/core/knowledge_base/chunking/__init__.py +11 -0
- astrbot/core/knowledge_base/chunking/base.py +24 -0
- astrbot/core/knowledge_base/chunking/fixed_size.py +57 -0
- astrbot/core/knowledge_base/chunking/recursive.py +155 -0
- astrbot/core/knowledge_base/kb_db_sqlite.py +299 -0
- astrbot/core/knowledge_base/kb_helper.py +348 -0
- astrbot/core/knowledge_base/kb_mgr.py +287 -0
- astrbot/core/knowledge_base/models.py +114 -0
- astrbot/core/knowledge_base/parsers/__init__.py +15 -0
- astrbot/core/knowledge_base/parsers/base.py +50 -0
- astrbot/core/knowledge_base/parsers/markitdown_parser.py +25 -0
- astrbot/core/knowledge_base/parsers/pdf_parser.py +100 -0
- astrbot/core/knowledge_base/parsers/text_parser.py +41 -0
- astrbot/core/knowledge_base/parsers/util.py +13 -0
- astrbot/core/knowledge_base/retrieval/__init__.py +16 -0
- astrbot/core/knowledge_base/retrieval/hit_stopwords.txt +767 -0
- astrbot/core/knowledge_base/retrieval/manager.py +273 -0
- astrbot/core/knowledge_base/retrieval/rank_fusion.py +138 -0
- astrbot/core/knowledge_base/retrieval/sparse_retriever.py +130 -0
- astrbot/core/pipeline/process_stage/method/llm_request.py +29 -7
- astrbot/core/pipeline/process_stage/utils.py +80 -0
- astrbot/core/platform/astr_message_event.py +8 -7
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +5 -2
- astrbot/core/platform/sources/misskey/misskey_adapter.py +380 -44
- astrbot/core/platform/sources/misskey/misskey_api.py +581 -45
- astrbot/core/platform/sources/misskey/misskey_event.py +76 -41
- astrbot/core/platform/sources/misskey/misskey_utils.py +254 -43
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +2 -1
- astrbot/core/platform/sources/satori/satori_adapter.py +27 -1
- astrbot/core/platform/sources/satori/satori_event.py +270 -99
- astrbot/core/provider/manager.py +22 -9
- astrbot/core/provider/provider.py +67 -0
- astrbot/core/provider/sources/anthropic_source.py +4 -4
- astrbot/core/provider/sources/dashscope_source.py +10 -9
- astrbot/core/provider/sources/dify_source.py +6 -8
- astrbot/core/provider/sources/gemini_embedding_source.py +1 -2
- astrbot/core/provider/sources/openai_embedding_source.py +1 -2
- astrbot/core/provider/sources/openai_source.py +43 -15
- astrbot/core/provider/sources/openai_tts_api_source.py +1 -1
- astrbot/core/provider/sources/xinference_rerank_source.py +108 -0
- astrbot/core/provider/sources/xinference_stt_provider.py +187 -0
- astrbot/core/star/context.py +19 -13
- astrbot/core/star/star.py +6 -0
- astrbot/core/star/star_manager.py +13 -7
- astrbot/core/umop_config_router.py +81 -0
- astrbot/core/updator.py +1 -1
- astrbot/core/utils/io.py +23 -12
- astrbot/dashboard/routes/__init__.py +2 -0
- astrbot/dashboard/routes/config.py +137 -9
- astrbot/dashboard/routes/knowledge_base.py +1065 -0
- astrbot/dashboard/routes/plugin.py +24 -5
- astrbot/dashboard/routes/update.py +1 -1
- astrbot/dashboard/server.py +6 -0
- astrbot/dashboard/utils.py +161 -0
- {astrbot-4.3.5.dist-info → astrbot-4.5.1.dist-info}/METADATA +30 -13
- {astrbot-4.3.5.dist-info → astrbot-4.5.1.dist-info}/RECORD +72 -46
- {astrbot-4.3.5.dist-info → astrbot-4.5.1.dist-info}/WHEEL +0 -0
- {astrbot-4.3.5.dist-info → astrbot-4.5.1.dist-info}/entry_points.txt +0 -0
- {astrbot-4.3.5.dist-info → astrbot-4.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import
|
|
3
|
-
from typing import Dict, Any, Optional, Awaitable
|
|
2
|
+
import random
|
|
3
|
+
from typing import Dict, Any, Optional, Awaitable, List
|
|
4
4
|
|
|
5
5
|
from astrbot.api import logger
|
|
6
6
|
from astrbot.api.event import MessageChain
|
|
@@ -14,6 +14,13 @@ from astrbot.core.platform.astr_message_event import MessageSession
|
|
|
14
14
|
import astrbot.api.message_components as Comp
|
|
15
15
|
|
|
16
16
|
from .misskey_api import MisskeyAPI
|
|
17
|
+
import os
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import magic # type: ignore
|
|
21
|
+
except Exception:
|
|
22
|
+
magic = None
|
|
23
|
+
|
|
17
24
|
from .misskey_event import MisskeyPlatformEvent
|
|
18
25
|
from .misskey_utils import (
|
|
19
26
|
serialize_message_chain,
|
|
@@ -25,9 +32,15 @@ from .misskey_utils import (
|
|
|
25
32
|
extract_sender_info,
|
|
26
33
|
create_base_message,
|
|
27
34
|
process_at_mention,
|
|
35
|
+
format_poll,
|
|
28
36
|
cache_user_info,
|
|
29
37
|
cache_room_info,
|
|
30
38
|
)
|
|
39
|
+
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
|
40
|
+
|
|
41
|
+
# Constants
|
|
42
|
+
MAX_FILE_UPLOAD_COUNT = 16
|
|
43
|
+
DEFAULT_UPLOAD_CONCURRENCY = 3
|
|
31
44
|
|
|
32
45
|
|
|
33
46
|
@register_platform_adapter("misskey", "Misskey 平台适配器")
|
|
@@ -46,6 +59,31 @@ class MisskeyPlatformAdapter(Platform):
|
|
|
46
59
|
)
|
|
47
60
|
self.local_only = self.config.get("misskey_local_only", False)
|
|
48
61
|
self.enable_chat = self.config.get("misskey_enable_chat", True)
|
|
62
|
+
self.enable_file_upload = self.config.get("misskey_enable_file_upload", True)
|
|
63
|
+
self.upload_folder = self.config.get("misskey_upload_folder")
|
|
64
|
+
|
|
65
|
+
# download / security related options (exposed to platform_config)
|
|
66
|
+
self.allow_insecure_downloads = bool(
|
|
67
|
+
self.config.get("misskey_allow_insecure_downloads", False)
|
|
68
|
+
)
|
|
69
|
+
# parse download timeout and chunk size safely
|
|
70
|
+
_dt = self.config.get("misskey_download_timeout")
|
|
71
|
+
try:
|
|
72
|
+
self.download_timeout = int(_dt) if _dt is not None else 15
|
|
73
|
+
except Exception:
|
|
74
|
+
self.download_timeout = 15
|
|
75
|
+
|
|
76
|
+
_chunk = self.config.get("misskey_download_chunk_size")
|
|
77
|
+
try:
|
|
78
|
+
self.download_chunk_size = int(_chunk) if _chunk is not None else 64 * 1024
|
|
79
|
+
except Exception:
|
|
80
|
+
self.download_chunk_size = 64 * 1024
|
|
81
|
+
# parse max download bytes safely
|
|
82
|
+
_md_bytes = self.config.get("misskey_max_download_bytes")
|
|
83
|
+
try:
|
|
84
|
+
self.max_download_bytes = int(_md_bytes) if _md_bytes is not None else None
|
|
85
|
+
except Exception:
|
|
86
|
+
self.max_download_bytes = None
|
|
49
87
|
|
|
50
88
|
self.unique_session = platform_settings["unique_session"]
|
|
51
89
|
|
|
@@ -63,6 +101,11 @@ class MisskeyPlatformAdapter(Platform):
|
|
|
63
101
|
"misskey_default_visibility": "public",
|
|
64
102
|
"misskey_local_only": False,
|
|
65
103
|
"misskey_enable_chat": True,
|
|
104
|
+
# download / security options
|
|
105
|
+
"misskey_allow_insecure_downloads": False,
|
|
106
|
+
"misskey_download_timeout": 15,
|
|
107
|
+
"misskey_download_chunk_size": 65536,
|
|
108
|
+
"misskey_max_download_bytes": None,
|
|
66
109
|
}
|
|
67
110
|
default_config.update(self.config)
|
|
68
111
|
|
|
@@ -78,7 +121,14 @@ class MisskeyPlatformAdapter(Platform):
|
|
|
78
121
|
logger.error("[Misskey] 配置不完整,无法启动")
|
|
79
122
|
return
|
|
80
123
|
|
|
81
|
-
self.api = MisskeyAPI(
|
|
124
|
+
self.api = MisskeyAPI(
|
|
125
|
+
self.instance_url,
|
|
126
|
+
self.access_token,
|
|
127
|
+
allow_insecure_downloads=self.allow_insecure_downloads,
|
|
128
|
+
download_timeout=self.download_timeout,
|
|
129
|
+
chunk_size=self.download_chunk_size,
|
|
130
|
+
max_download_bytes=self.max_download_bytes,
|
|
131
|
+
)
|
|
82
132
|
self._running = True
|
|
83
133
|
|
|
84
134
|
try:
|
|
@@ -95,6 +145,80 @@ class MisskeyPlatformAdapter(Platform):
|
|
|
95
145
|
|
|
96
146
|
await self._start_websocket_connection()
|
|
97
147
|
|
|
148
|
+
def _register_event_handlers(self, streaming):
|
|
149
|
+
"""注册事件处理器"""
|
|
150
|
+
streaming.add_message_handler("notification", self._handle_notification)
|
|
151
|
+
streaming.add_message_handler("main:notification", self._handle_notification)
|
|
152
|
+
|
|
153
|
+
if self.enable_chat:
|
|
154
|
+
streaming.add_message_handler("newChatMessage", self._handle_chat_message)
|
|
155
|
+
streaming.add_message_handler(
|
|
156
|
+
"messaging:newChatMessage", self._handle_chat_message
|
|
157
|
+
)
|
|
158
|
+
streaming.add_message_handler("_debug", self._debug_handler)
|
|
159
|
+
|
|
160
|
+
async def _send_text_only_message(
|
|
161
|
+
self, session_id: str, text: str, session, message_chain
|
|
162
|
+
):
|
|
163
|
+
"""发送纯文本消息(无文件上传)"""
|
|
164
|
+
if not self.api:
|
|
165
|
+
return await super().send_by_session(session, message_chain)
|
|
166
|
+
|
|
167
|
+
if session_id and is_valid_user_session_id(session_id):
|
|
168
|
+
from .misskey_utils import extract_user_id_from_session_id
|
|
169
|
+
|
|
170
|
+
user_id = extract_user_id_from_session_id(session_id)
|
|
171
|
+
payload: Dict[str, Any] = {"toUserId": user_id, "text": text}
|
|
172
|
+
await self.api.send_message(payload)
|
|
173
|
+
elif session_id and is_valid_room_session_id(session_id):
|
|
174
|
+
from .misskey_utils import extract_room_id_from_session_id
|
|
175
|
+
|
|
176
|
+
room_id = extract_room_id_from_session_id(session_id)
|
|
177
|
+
payload = {"toRoomId": room_id, "text": text}
|
|
178
|
+
await self.api.send_room_message(payload)
|
|
179
|
+
|
|
180
|
+
return await super().send_by_session(session, message_chain)
|
|
181
|
+
|
|
182
|
+
def _process_poll_data(
|
|
183
|
+
self, message: AstrBotMessage, poll: Dict[str, Any], message_parts: List[str]
|
|
184
|
+
):
|
|
185
|
+
"""处理投票数据,将其添加到消息中"""
|
|
186
|
+
try:
|
|
187
|
+
if not isinstance(message.raw_message, dict):
|
|
188
|
+
message.raw_message = {}
|
|
189
|
+
message.raw_message["poll"] = poll
|
|
190
|
+
setattr(message, "poll", poll)
|
|
191
|
+
except Exception:
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
poll_text = format_poll(poll)
|
|
195
|
+
if poll_text:
|
|
196
|
+
message.message.append(Comp.Plain(poll_text))
|
|
197
|
+
message_parts.append(poll_text)
|
|
198
|
+
|
|
199
|
+
def _extract_additional_fields(self, session, message_chain) -> Dict[str, Any]:
|
|
200
|
+
"""从会话和消息链中提取额外字段"""
|
|
201
|
+
fields = {"cw": None, "poll": None, "renote_id": None, "channel_id": None}
|
|
202
|
+
|
|
203
|
+
for comp in message_chain.chain:
|
|
204
|
+
if hasattr(comp, "cw") and getattr(comp, "cw", None):
|
|
205
|
+
fields["cw"] = getattr(comp, "cw")
|
|
206
|
+
break
|
|
207
|
+
|
|
208
|
+
if hasattr(session, "extra_data") and isinstance(
|
|
209
|
+
getattr(session, "extra_data", None), dict
|
|
210
|
+
):
|
|
211
|
+
extra_data = getattr(session, "extra_data")
|
|
212
|
+
fields.update(
|
|
213
|
+
{
|
|
214
|
+
"poll": extra_data.get("poll"),
|
|
215
|
+
"renote_id": extra_data.get("renote_id"),
|
|
216
|
+
"channel_id": extra_data.get("channel_id"),
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return fields
|
|
221
|
+
|
|
98
222
|
async def _start_websocket_connection(self):
|
|
99
223
|
backoff_delay = 1.0
|
|
100
224
|
max_backoff = 300.0
|
|
@@ -109,25 +233,20 @@ class MisskeyPlatformAdapter(Platform):
|
|
|
109
233
|
break
|
|
110
234
|
|
|
111
235
|
streaming = self.api.get_streaming_client()
|
|
112
|
-
|
|
113
|
-
if self.enable_chat:
|
|
114
|
-
streaming.add_message_handler(
|
|
115
|
-
"newChatMessage", self._handle_chat_message
|
|
116
|
-
)
|
|
117
|
-
streaming.add_message_handler("_debug", self._debug_handler)
|
|
236
|
+
self._register_event_handlers(streaming)
|
|
118
237
|
|
|
119
238
|
if await streaming.connect():
|
|
120
239
|
logger.info(
|
|
121
240
|
f"[Misskey] WebSocket 已连接 (尝试 #{connection_attempts})"
|
|
122
241
|
)
|
|
123
|
-
connection_attempts = 0
|
|
242
|
+
connection_attempts = 0
|
|
124
243
|
await streaming.subscribe_channel("main")
|
|
125
244
|
if self.enable_chat:
|
|
126
245
|
await streaming.subscribe_channel("messaging")
|
|
127
246
|
await streaming.subscribe_channel("messagingIndex")
|
|
128
247
|
logger.info("[Misskey] 聊天频道已订阅")
|
|
129
248
|
|
|
130
|
-
backoff_delay = 1.0
|
|
249
|
+
backoff_delay = 1.0
|
|
131
250
|
await streaming.listen()
|
|
132
251
|
else:
|
|
133
252
|
logger.error(
|
|
@@ -140,18 +259,20 @@ class MisskeyPlatformAdapter(Platform):
|
|
|
140
259
|
)
|
|
141
260
|
|
|
142
261
|
if self._running:
|
|
262
|
+
jitter = random.uniform(0, 1.0)
|
|
263
|
+
sleep_time = backoff_delay + jitter
|
|
143
264
|
logger.info(
|
|
144
|
-
f"[Misskey] {
|
|
265
|
+
f"[Misskey] {sleep_time:.1f}秒后重连 (下次尝试 #{connection_attempts + 1})"
|
|
145
266
|
)
|
|
146
|
-
await asyncio.sleep(
|
|
267
|
+
await asyncio.sleep(sleep_time)
|
|
147
268
|
backoff_delay = min(backoff_delay * backoff_multiplier, max_backoff)
|
|
148
269
|
|
|
149
270
|
async def _handle_notification(self, data: Dict[str, Any]):
|
|
150
271
|
try:
|
|
272
|
+
notification_type = data.get("type")
|
|
151
273
|
logger.debug(
|
|
152
|
-
f"[Misskey]
|
|
274
|
+
f"[Misskey] 收到通知事件: type={notification_type}, user_id={data.get('userId', 'unknown')}"
|
|
153
275
|
)
|
|
154
|
-
notification_type = data.get("type")
|
|
155
276
|
if notification_type in ["mention", "reply", "quote"]:
|
|
156
277
|
note = data.get("note")
|
|
157
278
|
if note and self._is_bot_mentioned(note):
|
|
@@ -164,7 +285,7 @@ class MisskeyPlatformAdapter(Platform):
|
|
|
164
285
|
message_obj=message,
|
|
165
286
|
platform_meta=self.meta(),
|
|
166
287
|
session_id=message.session_id,
|
|
167
|
-
client=self
|
|
288
|
+
client=self,
|
|
168
289
|
)
|
|
169
290
|
self.commit_event(event)
|
|
170
291
|
except Exception as e:
|
|
@@ -172,17 +293,16 @@ class MisskeyPlatformAdapter(Platform):
|
|
|
172
293
|
|
|
173
294
|
async def _handle_chat_message(self, data: Dict[str, Any]):
|
|
174
295
|
try:
|
|
175
|
-
logger.debug(
|
|
176
|
-
f"[Misskey] 收到聊天事件数据:\n{json.dumps(data, indent=2, ensure_ascii=False)}"
|
|
177
|
-
)
|
|
178
|
-
|
|
179
296
|
sender_id = str(
|
|
180
297
|
data.get("fromUserId", "") or data.get("fromUser", {}).get("id", "")
|
|
181
298
|
)
|
|
299
|
+
room_id = data.get("toRoomId")
|
|
300
|
+
logger.debug(
|
|
301
|
+
f"[Misskey] 收到聊天事件: sender_id={sender_id}, room_id={room_id}, is_self={sender_id == self.client_self_id}"
|
|
302
|
+
)
|
|
182
303
|
if sender_id == self.client_self_id:
|
|
183
304
|
return
|
|
184
305
|
|
|
185
|
-
room_id = data.get("toRoomId")
|
|
186
306
|
if room_id:
|
|
187
307
|
raw_text = data.get("text", "")
|
|
188
308
|
logger.debug(
|
|
@@ -200,15 +320,16 @@ class MisskeyPlatformAdapter(Platform):
|
|
|
200
320
|
message_obj=message,
|
|
201
321
|
platform_meta=self.meta(),
|
|
202
322
|
session_id=message.session_id,
|
|
203
|
-
client=self
|
|
323
|
+
client=self,
|
|
204
324
|
)
|
|
205
325
|
self.commit_event(event)
|
|
206
326
|
except Exception as e:
|
|
207
327
|
logger.error(f"[Misskey] 处理聊天消息失败: {e}")
|
|
208
328
|
|
|
209
329
|
async def _debug_handler(self, data: Dict[str, Any]):
|
|
330
|
+
event_type = data.get("type", "unknown")
|
|
210
331
|
logger.debug(
|
|
211
|
-
f"[Misskey]
|
|
332
|
+
f"[Misskey] 收到未处理事件: type={event_type}, channel={data.get('channel', 'unknown')}"
|
|
212
333
|
)
|
|
213
334
|
|
|
214
335
|
def _is_bot_mentioned(self, note: Dict[str, Any]) -> bool:
|
|
@@ -239,43 +360,250 @@ class MisskeyPlatformAdapter(Platform):
|
|
|
239
360
|
|
|
240
361
|
try:
|
|
241
362
|
session_id = session.session_id
|
|
363
|
+
|
|
242
364
|
text, has_at_user = serialize_message_chain(message_chain.chain)
|
|
243
365
|
|
|
244
366
|
if not has_at_user and session_id:
|
|
245
|
-
|
|
367
|
+
# 从session_id中提取用户ID用于缓存查询
|
|
368
|
+
# session_id格式为: "chat%<user_id>" 或 "room%<room_id>" 或 "note%<user_id>"
|
|
369
|
+
user_id_for_cache = None
|
|
370
|
+
if "%" in session_id:
|
|
371
|
+
parts = session_id.split("%")
|
|
372
|
+
if len(parts) >= 2:
|
|
373
|
+
user_id_for_cache = parts[1]
|
|
374
|
+
|
|
375
|
+
user_info = None
|
|
376
|
+
if user_id_for_cache:
|
|
377
|
+
user_info = self._user_cache.get(user_id_for_cache)
|
|
378
|
+
|
|
246
379
|
text = add_at_mention_if_needed(text, user_info, has_at_user)
|
|
247
380
|
|
|
381
|
+
# 检查是否有文件组件
|
|
382
|
+
has_file_components = any(
|
|
383
|
+
isinstance(comp, Comp.Image)
|
|
384
|
+
or isinstance(comp, Comp.File)
|
|
385
|
+
or hasattr(comp, "convert_to_file_path")
|
|
386
|
+
or hasattr(comp, "get_file")
|
|
387
|
+
or any(
|
|
388
|
+
hasattr(comp, a) for a in ("file", "url", "path", "src", "source")
|
|
389
|
+
)
|
|
390
|
+
for comp in message_chain.chain
|
|
391
|
+
)
|
|
392
|
+
|
|
248
393
|
if not text or not text.strip():
|
|
249
|
-
|
|
250
|
-
|
|
394
|
+
if not has_file_components:
|
|
395
|
+
logger.warning("[Misskey] 消息内容为空且无文件组件,跳过发送")
|
|
396
|
+
return await super().send_by_session(session, message_chain)
|
|
397
|
+
else:
|
|
398
|
+
text = ""
|
|
251
399
|
|
|
252
400
|
if len(text) > self.max_message_length:
|
|
253
401
|
text = text[: self.max_message_length] + "..."
|
|
254
402
|
|
|
255
|
-
|
|
256
|
-
|
|
403
|
+
file_ids: List[str] = []
|
|
404
|
+
fallback_urls: List[str] = []
|
|
405
|
+
|
|
406
|
+
if not self.enable_file_upload:
|
|
407
|
+
return await self._send_text_only_message(
|
|
408
|
+
session_id, text, session, message_chain
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
MAX_UPLOAD_CONCURRENCY = 10
|
|
412
|
+
upload_concurrency = int(
|
|
413
|
+
self.config.get(
|
|
414
|
+
"misskey_upload_concurrency", DEFAULT_UPLOAD_CONCURRENCY
|
|
415
|
+
)
|
|
416
|
+
)
|
|
417
|
+
upload_concurrency = min(upload_concurrency, MAX_UPLOAD_CONCURRENCY)
|
|
418
|
+
sem = asyncio.Semaphore(upload_concurrency)
|
|
419
|
+
|
|
420
|
+
async def _upload_comp(comp) -> Optional[object]:
|
|
421
|
+
"""组件上传函数:处理 URL(下载后上传)或本地文件(直接上传)"""
|
|
422
|
+
from .misskey_utils import (
|
|
423
|
+
resolve_component_url_or_path,
|
|
424
|
+
upload_local_with_retries,
|
|
425
|
+
)
|
|
257
426
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
427
|
+
local_path = None
|
|
428
|
+
try:
|
|
429
|
+
async with sem:
|
|
430
|
+
if not self.api:
|
|
431
|
+
return None
|
|
432
|
+
|
|
433
|
+
# 解析组件的 URL 或本地路径
|
|
434
|
+
url_candidate, local_path = await resolve_component_url_or_path(
|
|
435
|
+
comp
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
if not url_candidate and not local_path:
|
|
439
|
+
return None
|
|
440
|
+
|
|
441
|
+
preferred_name = getattr(comp, "name", None) or getattr(
|
|
442
|
+
comp, "file", None
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# URL 上传:下载后本地上传
|
|
446
|
+
if url_candidate:
|
|
447
|
+
result = await self.api.upload_and_find_file(
|
|
448
|
+
str(url_candidate),
|
|
449
|
+
preferred_name,
|
|
450
|
+
folder_id=self.upload_folder,
|
|
451
|
+
)
|
|
452
|
+
if isinstance(result, dict) and result.get("id"):
|
|
453
|
+
return str(result["id"])
|
|
454
|
+
|
|
455
|
+
# 本地文件上传
|
|
456
|
+
if local_path:
|
|
457
|
+
file_id = await upload_local_with_retries(
|
|
458
|
+
self.api,
|
|
459
|
+
str(local_path),
|
|
460
|
+
preferred_name,
|
|
461
|
+
self.upload_folder,
|
|
462
|
+
)
|
|
463
|
+
if file_id:
|
|
464
|
+
return file_id
|
|
465
|
+
|
|
466
|
+
# 所有上传都失败,尝试获取 URL 作为回退
|
|
467
|
+
if hasattr(comp, "register_to_file_service"):
|
|
468
|
+
try:
|
|
469
|
+
url = await comp.register_to_file_service()
|
|
470
|
+
if url:
|
|
471
|
+
return {"fallback_url": url}
|
|
472
|
+
except Exception:
|
|
473
|
+
pass
|
|
474
|
+
|
|
475
|
+
return None
|
|
476
|
+
|
|
477
|
+
finally:
|
|
478
|
+
# 清理临时文件
|
|
479
|
+
if local_path and isinstance(local_path, str):
|
|
480
|
+
data_temp = os.path.join(get_astrbot_data_path(), "temp")
|
|
481
|
+
if local_path.startswith(data_temp) and os.path.exists(
|
|
482
|
+
local_path
|
|
483
|
+
):
|
|
484
|
+
try:
|
|
485
|
+
os.remove(local_path)
|
|
486
|
+
logger.debug(f"[Misskey] 已清理临时文件: {local_path}")
|
|
487
|
+
except Exception:
|
|
488
|
+
pass
|
|
489
|
+
|
|
490
|
+
# 收集所有可能包含文件/URL信息的组件:支持异步接口或同步字段
|
|
491
|
+
file_components = []
|
|
492
|
+
for comp in message_chain.chain:
|
|
493
|
+
try:
|
|
494
|
+
if (
|
|
495
|
+
isinstance(comp, Comp.Image)
|
|
496
|
+
or isinstance(comp, Comp.File)
|
|
497
|
+
or hasattr(comp, "convert_to_file_path")
|
|
498
|
+
or hasattr(comp, "get_file")
|
|
499
|
+
or any(
|
|
500
|
+
hasattr(comp, a)
|
|
501
|
+
for a in ("file", "url", "path", "src", "source")
|
|
502
|
+
)
|
|
503
|
+
):
|
|
504
|
+
file_components.append(comp)
|
|
505
|
+
except Exception:
|
|
506
|
+
# 保守跳过无法访问属性的组件
|
|
507
|
+
continue
|
|
508
|
+
|
|
509
|
+
if len(file_components) > MAX_FILE_UPLOAD_COUNT:
|
|
510
|
+
logger.warning(
|
|
511
|
+
f"[Misskey] 文件数量超过限制 ({len(file_components)} > {MAX_FILE_UPLOAD_COUNT}),只上传前{MAX_FILE_UPLOAD_COUNT}个文件"
|
|
512
|
+
)
|
|
513
|
+
file_components = file_components[:MAX_FILE_UPLOAD_COUNT]
|
|
514
|
+
|
|
515
|
+
upload_tasks = [_upload_comp(comp) for comp in file_components]
|
|
516
|
+
|
|
517
|
+
try:
|
|
518
|
+
results = await asyncio.gather(*upload_tasks) if upload_tasks else []
|
|
519
|
+
for r in results:
|
|
520
|
+
if not r:
|
|
521
|
+
continue
|
|
522
|
+
if isinstance(r, dict) and r.get("fallback_url"):
|
|
523
|
+
url = r.get("fallback_url")
|
|
524
|
+
if url:
|
|
525
|
+
fallback_urls.append(str(url))
|
|
526
|
+
else:
|
|
527
|
+
try:
|
|
528
|
+
fid_str = str(r)
|
|
529
|
+
if fid_str:
|
|
530
|
+
file_ids.append(fid_str)
|
|
531
|
+
except Exception:
|
|
532
|
+
pass
|
|
533
|
+
except Exception:
|
|
534
|
+
logger.debug("[Misskey] 并发上传过程中出现异常,继续发送文本")
|
|
535
|
+
|
|
536
|
+
if session_id and is_valid_room_session_id(session_id):
|
|
261
537
|
from .misskey_utils import extract_room_id_from_session_id
|
|
262
538
|
|
|
263
539
|
room_id = extract_room_id_from_session_id(session_id)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
540
|
+
if fallback_urls:
|
|
541
|
+
appended = "\n" + "\n".join(fallback_urls)
|
|
542
|
+
text = (text or "") + appended
|
|
543
|
+
payload: Dict[str, Any] = {"toRoomId": room_id, "text": text}
|
|
544
|
+
if file_ids:
|
|
545
|
+
payload["fileIds"] = file_ids
|
|
546
|
+
await self.api.send_room_message(payload)
|
|
547
|
+
elif session_id:
|
|
548
|
+
from .misskey_utils import (
|
|
549
|
+
extract_user_id_from_session_id,
|
|
550
|
+
is_valid_chat_session_id,
|
|
271
551
|
)
|
|
272
552
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
553
|
+
if is_valid_chat_session_id(session_id):
|
|
554
|
+
user_id = extract_user_id_from_session_id(session_id)
|
|
555
|
+
if fallback_urls:
|
|
556
|
+
appended = "\n" + "\n".join(fallback_urls)
|
|
557
|
+
text = (text or "") + appended
|
|
558
|
+
payload: Dict[str, Any] = {"toUserId": user_id, "text": text}
|
|
559
|
+
if file_ids:
|
|
560
|
+
# 聊天消息只支持单个文件,使用 fileId 而不是 fileIds
|
|
561
|
+
payload["fileId"] = file_ids[0]
|
|
562
|
+
if len(file_ids) > 1:
|
|
563
|
+
logger.warning(
|
|
564
|
+
f"[Misskey] 聊天消息只支持单个文件,忽略其余 {len(file_ids) - 1} 个文件"
|
|
565
|
+
)
|
|
566
|
+
await self.api.send_message(payload)
|
|
567
|
+
else:
|
|
568
|
+
# 回退到发帖逻辑
|
|
569
|
+
# 去掉 session_id 中的 note% 前缀以匹配 user_cache 的键格式
|
|
570
|
+
user_id_for_cache = (
|
|
571
|
+
session_id.split("%")[1] if "%" in session_id else session_id
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
# 获取用户缓存信息(包含reply_to_note_id)
|
|
575
|
+
user_info_for_reply = self._user_cache.get(user_id_for_cache, {})
|
|
576
|
+
|
|
577
|
+
visibility, visible_user_ids = resolve_message_visibility(
|
|
578
|
+
user_id=user_id_for_cache,
|
|
579
|
+
user_cache=self._user_cache,
|
|
580
|
+
self_id=self.client_self_id,
|
|
581
|
+
default_visibility=self.default_visibility,
|
|
582
|
+
)
|
|
583
|
+
logger.debug(
|
|
584
|
+
f"[Misskey] 解析可见性: visibility={visibility}, visible_user_ids={visible_user_ids}, session_id={session_id}, user_id_for_cache={user_id_for_cache}"
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
fields = self._extract_additional_fields(session, message_chain)
|
|
588
|
+
if fallback_urls:
|
|
589
|
+
appended = "\n" + "\n".join(fallback_urls)
|
|
590
|
+
text = (text or "") + appended
|
|
591
|
+
|
|
592
|
+
# 从缓存中获取原消息ID作为reply_id
|
|
593
|
+
reply_id = user_info_for_reply.get("reply_to_note_id")
|
|
594
|
+
|
|
595
|
+
await self.api.create_note(
|
|
596
|
+
text=text,
|
|
597
|
+
visibility=visibility,
|
|
598
|
+
visible_user_ids=visible_user_ids,
|
|
599
|
+
file_ids=file_ids or None,
|
|
600
|
+
local_only=self.local_only,
|
|
601
|
+
reply_id=reply_id, # 添加reply_id参数
|
|
602
|
+
cw=fields["cw"],
|
|
603
|
+
poll=fields["poll"],
|
|
604
|
+
renote_id=fields["renote_id"],
|
|
605
|
+
channel_id=fields["channel_id"],
|
|
606
|
+
)
|
|
279
607
|
|
|
280
608
|
except Exception as e:
|
|
281
609
|
logger.error(f"[Misskey] 发送消息失败: {e}")
|
|
@@ -309,6 +637,14 @@ class MisskeyPlatformAdapter(Platform):
|
|
|
309
637
|
file_parts = process_files(message, files)
|
|
310
638
|
message_parts.extend(file_parts)
|
|
311
639
|
|
|
640
|
+
poll = raw_data.get("poll") or (
|
|
641
|
+
raw_data.get("note", {}).get("poll")
|
|
642
|
+
if isinstance(raw_data.get("note"), dict)
|
|
643
|
+
else None
|
|
644
|
+
)
|
|
645
|
+
if poll and isinstance(poll, dict):
|
|
646
|
+
self._process_poll_data(message, poll, message_parts)
|
|
647
|
+
|
|
312
648
|
message.message_str = (
|
|
313
649
|
" ".join(part for part in message_parts if part.strip())
|
|
314
650
|
if message_parts
|