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.
Files changed (72) hide show
  1. astrbot/core/agent/runners/tool_loop_agent_runner.py +31 -2
  2. astrbot/core/astrbot_config_mgr.py +23 -51
  3. astrbot/core/config/default.py +132 -12
  4. astrbot/core/conversation_mgr.py +36 -1
  5. astrbot/core/core_lifecycle.py +24 -5
  6. astrbot/core/db/migration/helper.py +6 -3
  7. astrbot/core/db/migration/migra_45_to_46.py +44 -0
  8. astrbot/core/db/vec_db/base.py +33 -2
  9. astrbot/core/db/vec_db/faiss_impl/document_storage.py +310 -52
  10. astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +31 -3
  11. astrbot/core/db/vec_db/faiss_impl/vec_db.py +81 -23
  12. astrbot/core/file_token_service.py +6 -1
  13. astrbot/core/initial_loader.py +6 -3
  14. astrbot/core/knowledge_base/chunking/__init__.py +11 -0
  15. astrbot/core/knowledge_base/chunking/base.py +24 -0
  16. astrbot/core/knowledge_base/chunking/fixed_size.py +57 -0
  17. astrbot/core/knowledge_base/chunking/recursive.py +155 -0
  18. astrbot/core/knowledge_base/kb_db_sqlite.py +299 -0
  19. astrbot/core/knowledge_base/kb_helper.py +348 -0
  20. astrbot/core/knowledge_base/kb_mgr.py +287 -0
  21. astrbot/core/knowledge_base/models.py +114 -0
  22. astrbot/core/knowledge_base/parsers/__init__.py +15 -0
  23. astrbot/core/knowledge_base/parsers/base.py +50 -0
  24. astrbot/core/knowledge_base/parsers/markitdown_parser.py +25 -0
  25. astrbot/core/knowledge_base/parsers/pdf_parser.py +100 -0
  26. astrbot/core/knowledge_base/parsers/text_parser.py +41 -0
  27. astrbot/core/knowledge_base/parsers/util.py +13 -0
  28. astrbot/core/knowledge_base/retrieval/__init__.py +16 -0
  29. astrbot/core/knowledge_base/retrieval/hit_stopwords.txt +767 -0
  30. astrbot/core/knowledge_base/retrieval/manager.py +273 -0
  31. astrbot/core/knowledge_base/retrieval/rank_fusion.py +138 -0
  32. astrbot/core/knowledge_base/retrieval/sparse_retriever.py +130 -0
  33. astrbot/core/pipeline/process_stage/method/llm_request.py +29 -7
  34. astrbot/core/pipeline/process_stage/utils.py +80 -0
  35. astrbot/core/platform/astr_message_event.py +8 -7
  36. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +5 -2
  37. astrbot/core/platform/sources/misskey/misskey_adapter.py +380 -44
  38. astrbot/core/platform/sources/misskey/misskey_api.py +581 -45
  39. astrbot/core/platform/sources/misskey/misskey_event.py +76 -41
  40. astrbot/core/platform/sources/misskey/misskey_utils.py +254 -43
  41. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +2 -1
  42. astrbot/core/platform/sources/satori/satori_adapter.py +27 -1
  43. astrbot/core/platform/sources/satori/satori_event.py +270 -99
  44. astrbot/core/provider/manager.py +22 -9
  45. astrbot/core/provider/provider.py +67 -0
  46. astrbot/core/provider/sources/anthropic_source.py +4 -4
  47. astrbot/core/provider/sources/dashscope_source.py +10 -9
  48. astrbot/core/provider/sources/dify_source.py +6 -8
  49. astrbot/core/provider/sources/gemini_embedding_source.py +1 -2
  50. astrbot/core/provider/sources/openai_embedding_source.py +1 -2
  51. astrbot/core/provider/sources/openai_source.py +43 -15
  52. astrbot/core/provider/sources/openai_tts_api_source.py +1 -1
  53. astrbot/core/provider/sources/xinference_rerank_source.py +108 -0
  54. astrbot/core/provider/sources/xinference_stt_provider.py +187 -0
  55. astrbot/core/star/context.py +19 -13
  56. astrbot/core/star/star.py +6 -0
  57. astrbot/core/star/star_manager.py +13 -7
  58. astrbot/core/umop_config_router.py +81 -0
  59. astrbot/core/updator.py +1 -1
  60. astrbot/core/utils/io.py +23 -12
  61. astrbot/dashboard/routes/__init__.py +2 -0
  62. astrbot/dashboard/routes/config.py +137 -9
  63. astrbot/dashboard/routes/knowledge_base.py +1065 -0
  64. astrbot/dashboard/routes/plugin.py +24 -5
  65. astrbot/dashboard/routes/update.py +1 -1
  66. astrbot/dashboard/server.py +6 -0
  67. astrbot/dashboard/utils.py +161 -0
  68. {astrbot-4.3.5.dist-info → astrbot-4.5.1.dist-info}/METADATA +30 -13
  69. {astrbot-4.3.5.dist-info → astrbot-4.5.1.dist-info}/RECORD +72 -46
  70. {astrbot-4.3.5.dist-info → astrbot-4.5.1.dist-info}/WHEEL +0 -0
  71. {astrbot-4.3.5.dist-info → astrbot-4.5.1.dist-info}/entry_points.txt +0 -0
  72. {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 json
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(self.instance_url, self.access_token)
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
- streaming.add_message_handler("notification", self._handle_notification)
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] {backoff_delay:.1f}秒后重连 (下次尝试 #{connection_attempts + 1})"
265
+ f"[Misskey] {sleep_time:.1f}秒后重连 (下次尝试 #{connection_attempts + 1})"
145
266
  )
146
- await asyncio.sleep(backoff_delay)
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] 收到通知事件:\n{json.dumps(data, indent=2, ensure_ascii=False)}"
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.api,
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.api,
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] 收到未处理事件:\n{json.dumps(data, indent=2, ensure_ascii=False)}"
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
- user_info = self._user_cache.get(session_id)
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
- logger.warning("[Misskey] 消息内容为空,跳过发送")
250
- return await super().send_by_session(session, message_chain)
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
- if session_id and is_valid_user_session_id(session_id):
256
- from .misskey_utils import extract_user_id_from_session_id
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
- user_id = extract_user_id_from_session_id(session_id)
259
- await self.api.send_message(user_id, text)
260
- elif session_id and is_valid_room_session_id(session_id):
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
- await self.api.send_room_message(room_id, text)
265
- else:
266
- visibility, visible_user_ids = resolve_message_visibility(
267
- user_id=session_id,
268
- user_cache=self._user_cache,
269
- self_id=self.client_self_id,
270
- default_visibility=self.default_visibility,
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
- await self.api.create_note(
274
- text,
275
- visibility=visibility,
276
- visible_user_ids=visible_user_ids,
277
- local_only=self.local_only,
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