AstrBot 3.5.6__py3-none-any.whl → 4.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- astrbot/api/__init__.py +16 -4
- astrbot/api/all.py +2 -1
- astrbot/api/event/__init__.py +5 -6
- astrbot/api/event/filter/__init__.py +37 -34
- astrbot/api/platform/__init__.py +7 -8
- astrbot/api/provider/__init__.py +8 -7
- astrbot/api/star/__init__.py +3 -4
- astrbot/api/util/__init__.py +2 -2
- astrbot/cli/__init__.py +1 -0
- astrbot/cli/__main__.py +18 -197
- astrbot/cli/commands/__init__.py +6 -0
- astrbot/cli/commands/cmd_conf.py +209 -0
- astrbot/cli/commands/cmd_init.py +56 -0
- astrbot/cli/commands/cmd_plug.py +245 -0
- astrbot/cli/commands/cmd_run.py +62 -0
- astrbot/cli/utils/__init__.py +18 -0
- astrbot/cli/utils/basic.py +76 -0
- astrbot/cli/utils/plugin.py +246 -0
- astrbot/cli/utils/version_comparator.py +90 -0
- astrbot/core/__init__.py +17 -19
- astrbot/core/agent/agent.py +14 -0
- astrbot/core/agent/handoff.py +38 -0
- astrbot/core/agent/hooks.py +30 -0
- astrbot/core/agent/mcp_client.py +385 -0
- astrbot/core/agent/message.py +175 -0
- astrbot/core/agent/response.py +14 -0
- astrbot/core/agent/run_context.py +22 -0
- astrbot/core/agent/runners/__init__.py +3 -0
- astrbot/core/agent/runners/base.py +65 -0
- astrbot/core/agent/runners/coze/coze_agent_runner.py +367 -0
- astrbot/core/agent/runners/coze/coze_api_client.py +324 -0
- astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py +403 -0
- astrbot/core/agent/runners/dify/dify_agent_runner.py +336 -0
- astrbot/core/agent/runners/dify/dify_api_client.py +195 -0
- astrbot/core/agent/runners/tool_loop_agent_runner.py +400 -0
- astrbot/core/agent/tool.py +285 -0
- astrbot/core/agent/tool_executor.py +17 -0
- astrbot/core/astr_agent_context.py +19 -0
- astrbot/core/astr_agent_hooks.py +36 -0
- astrbot/core/astr_agent_run_util.py +80 -0
- astrbot/core/astr_agent_tool_exec.py +246 -0
- astrbot/core/astrbot_config_mgr.py +275 -0
- astrbot/core/config/__init__.py +2 -2
- astrbot/core/config/astrbot_config.py +60 -20
- astrbot/core/config/default.py +1972 -453
- astrbot/core/config/i18n_utils.py +110 -0
- astrbot/core/conversation_mgr.py +285 -75
- astrbot/core/core_lifecycle.py +167 -62
- astrbot/core/db/__init__.py +305 -102
- astrbot/core/db/migration/helper.py +69 -0
- astrbot/core/db/migration/migra_3_to_4.py +357 -0
- astrbot/core/db/migration/migra_45_to_46.py +44 -0
- astrbot/core/db/migration/migra_webchat_session.py +131 -0
- astrbot/core/db/migration/shared_preferences_v3.py +48 -0
- astrbot/core/db/migration/sqlite_v3.py +497 -0
- astrbot/core/db/po.py +259 -55
- astrbot/core/db/sqlite.py +773 -528
- astrbot/core/db/vec_db/base.py +73 -0
- astrbot/core/db/vec_db/faiss_impl/__init__.py +3 -0
- astrbot/core/db/vec_db/faiss_impl/document_storage.py +392 -0
- astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +93 -0
- astrbot/core/db/vec_db/faiss_impl/sqlite_init.sql +17 -0
- astrbot/core/db/vec_db/faiss_impl/vec_db.py +204 -0
- astrbot/core/event_bus.py +26 -22
- astrbot/core/exceptions.py +9 -0
- astrbot/core/file_token_service.py +98 -0
- astrbot/core/initial_loader.py +19 -10
- astrbot/core/knowledge_base/chunking/__init__.py +9 -0
- astrbot/core/knowledge_base/chunking/base.py +25 -0
- astrbot/core/knowledge_base/chunking/fixed_size.py +59 -0
- astrbot/core/knowledge_base/chunking/recursive.py +161 -0
- astrbot/core/knowledge_base/kb_db_sqlite.py +301 -0
- astrbot/core/knowledge_base/kb_helper.py +642 -0
- astrbot/core/knowledge_base/kb_mgr.py +330 -0
- astrbot/core/knowledge_base/models.py +120 -0
- astrbot/core/knowledge_base/parsers/__init__.py +13 -0
- astrbot/core/knowledge_base/parsers/base.py +51 -0
- astrbot/core/knowledge_base/parsers/markitdown_parser.py +26 -0
- astrbot/core/knowledge_base/parsers/pdf_parser.py +101 -0
- astrbot/core/knowledge_base/parsers/text_parser.py +42 -0
- astrbot/core/knowledge_base/parsers/url_parser.py +103 -0
- astrbot/core/knowledge_base/parsers/util.py +13 -0
- astrbot/core/knowledge_base/prompts.py +65 -0
- astrbot/core/knowledge_base/retrieval/__init__.py +14 -0
- astrbot/core/knowledge_base/retrieval/hit_stopwords.txt +767 -0
- astrbot/core/knowledge_base/retrieval/manager.py +276 -0
- astrbot/core/knowledge_base/retrieval/rank_fusion.py +142 -0
- astrbot/core/knowledge_base/retrieval/sparse_retriever.py +136 -0
- astrbot/core/log.py +21 -15
- astrbot/core/message/components.py +413 -287
- astrbot/core/message/message_event_result.py +35 -24
- astrbot/core/persona_mgr.py +192 -0
- astrbot/core/pipeline/__init__.py +14 -14
- 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 +13 -14
- astrbot/core/pipeline/content_safety_check/strategies/keywords.py +2 -1
- astrbot/core/pipeline/content_safety_check/strategies/strategy.py +6 -6
- astrbot/core/pipeline/context.py +7 -1
- astrbot/core/pipeline/context_utils.py +107 -0
- astrbot/core/pipeline/preprocess_stage/stage.py +63 -36
- astrbot/core/pipeline/process_stage/method/agent_request.py +48 -0
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +464 -0
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +202 -0
- astrbot/core/pipeline/process_stage/method/star_request.py +26 -32
- astrbot/core/pipeline/process_stage/stage.py +21 -15
- astrbot/core/pipeline/process_stage/utils.py +125 -0
- astrbot/core/pipeline/rate_limit_check/stage.py +34 -36
- astrbot/core/pipeline/respond/stage.py +142 -101
- astrbot/core/pipeline/result_decorate/stage.py +124 -57
- astrbot/core/pipeline/scheduler.py +21 -16
- astrbot/core/pipeline/session_status_check/stage.py +37 -0
- astrbot/core/pipeline/stage.py +11 -76
- astrbot/core/pipeline/waking_check/stage.py +69 -33
- astrbot/core/pipeline/whitelist_check/stage.py +10 -7
- astrbot/core/platform/__init__.py +6 -6
- astrbot/core/platform/astr_message_event.py +107 -129
- astrbot/core/platform/astrbot_message.py +32 -12
- astrbot/core/platform/manager.py +62 -18
- astrbot/core/platform/message_session.py +30 -0
- astrbot/core/platform/platform.py +16 -24
- astrbot/core/platform/platform_metadata.py +9 -4
- astrbot/core/platform/register.py +12 -7
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +136 -60
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +126 -46
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +63 -31
- astrbot/core/platform/sources/dingtalk/dingtalk_event.py +30 -26
- astrbot/core/platform/sources/discord/client.py +129 -0
- astrbot/core/platform/sources/discord/components.py +139 -0
- astrbot/core/platform/sources/discord/discord_platform_adapter.py +473 -0
- astrbot/core/platform/sources/discord/discord_platform_event.py +313 -0
- astrbot/core/platform/sources/lark/lark_adapter.py +27 -18
- astrbot/core/platform/sources/lark/lark_event.py +39 -13
- astrbot/core/platform/sources/misskey/misskey_adapter.py +770 -0
- astrbot/core/platform/sources/misskey/misskey_api.py +964 -0
- astrbot/core/platform/sources/misskey/misskey_event.py +163 -0
- astrbot/core/platform/sources/misskey/misskey_utils.py +550 -0
- astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +149 -33
- 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 +14 -8
- astrbot/core/platform/sources/satori/satori_adapter.py +792 -0
- astrbot/core/platform/sources/satori/satori_event.py +432 -0
- astrbot/core/platform/sources/slack/client.py +164 -0
- astrbot/core/platform/sources/slack/slack_adapter.py +416 -0
- astrbot/core/platform/sources/slack/slack_event.py +253 -0
- astrbot/core/platform/sources/telegram/tg_adapter.py +100 -43
- astrbot/core/platform/sources/telegram/tg_event.py +136 -36
- astrbot/core/platform/sources/webchat/webchat_adapter.py +72 -22
- astrbot/core/platform/sources/webchat/webchat_event.py +46 -22
- astrbot/core/platform/sources/webchat/webchat_queue_mgr.py +35 -0
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +926 -0
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +178 -0
- astrbot/core/platform/sources/wechatpadpro/xml_data_parser.py +159 -0
- astrbot/core/platform/sources/wecom/wecom_adapter.py +169 -27
- astrbot/core/platform/sources/wecom/wecom_event.py +162 -77
- astrbot/core/platform/sources/wecom/wecom_kf.py +279 -0
- astrbot/core/platform/sources/wecom/wecom_kf_message.py +196 -0
- astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py +297 -0
- astrbot/core/platform/sources/wecom_ai_bot/__init__.py +15 -0
- astrbot/core/platform/sources/wecom_ai_bot/ierror.py +19 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +472 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py +417 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +152 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py +153 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +168 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_utils.py +209 -0
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +306 -0
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +186 -0
- astrbot/core/platform_message_history_mgr.py +49 -0
- astrbot/core/provider/__init__.py +2 -3
- astrbot/core/provider/entites.py +8 -8
- astrbot/core/provider/entities.py +154 -98
- astrbot/core/provider/func_tool_manager.py +446 -458
- astrbot/core/provider/manager.py +345 -207
- astrbot/core/provider/provider.py +188 -73
- astrbot/core/provider/register.py +9 -7
- astrbot/core/provider/sources/anthropic_source.py +295 -115
- astrbot/core/provider/sources/azure_tts_source.py +224 -0
- astrbot/core/provider/sources/bailian_rerank_source.py +236 -0
- astrbot/core/provider/sources/dashscope_tts.py +138 -14
- astrbot/core/provider/sources/edge_tts_source.py +24 -19
- astrbot/core/provider/sources/fishaudio_tts_api_source.py +58 -13
- astrbot/core/provider/sources/gemini_embedding_source.py +61 -0
- astrbot/core/provider/sources/gemini_source.py +310 -132
- astrbot/core/provider/sources/gemini_tts_source.py +81 -0
- astrbot/core/provider/sources/groq_source.py +15 -0
- astrbot/core/provider/sources/gsv_selfhosted_source.py +151 -0
- astrbot/core/provider/sources/gsvi_tts_source.py +14 -7
- astrbot/core/provider/sources/minimax_tts_api_source.py +159 -0
- astrbot/core/provider/sources/openai_embedding_source.py +40 -0
- astrbot/core/provider/sources/openai_source.py +241 -145
- astrbot/core/provider/sources/openai_tts_api_source.py +18 -7
- astrbot/core/provider/sources/sensevoice_selfhosted_source.py +13 -11
- astrbot/core/provider/sources/vllm_rerank_source.py +71 -0
- astrbot/core/provider/sources/volcengine_tts.py +115 -0
- astrbot/core/provider/sources/whisper_api_source.py +18 -13
- astrbot/core/provider/sources/whisper_selfhosted_source.py +19 -12
- astrbot/core/provider/sources/xinference_rerank_source.py +116 -0
- astrbot/core/provider/sources/xinference_stt_provider.py +197 -0
- astrbot/core/provider/sources/zhipu_source.py +6 -73
- astrbot/core/star/__init__.py +43 -11
- astrbot/core/star/config.py +17 -18
- astrbot/core/star/context.py +362 -138
- astrbot/core/star/filter/__init__.py +4 -3
- astrbot/core/star/filter/command.py +111 -35
- astrbot/core/star/filter/command_group.py +46 -34
- 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 +45 -12
- astrbot/core/star/filter/regex.py +4 -2
- astrbot/core/star/register/__init__.py +19 -15
- astrbot/core/star/register/star.py +41 -13
- astrbot/core/star/register/star_handler.py +236 -86
- astrbot/core/star/session_llm_manager.py +280 -0
- astrbot/core/star/session_plugin_manager.py +170 -0
- astrbot/core/star/star.py +36 -43
- astrbot/core/star/star_handler.py +47 -85
- astrbot/core/star/star_manager.py +442 -260
- astrbot/core/star/star_tools.py +167 -45
- astrbot/core/star/updator.py +17 -20
- astrbot/core/umop_config_router.py +106 -0
- astrbot/core/updator.py +38 -13
- astrbot/core/utils/astrbot_path.py +39 -0
- astrbot/core/utils/command_parser.py +1 -1
- astrbot/core/utils/io.py +119 -60
- astrbot/core/utils/log_pipe.py +1 -1
- astrbot/core/utils/metrics.py +11 -10
- astrbot/core/utils/migra_helper.py +73 -0
- astrbot/core/utils/path_util.py +63 -62
- astrbot/core/utils/pip_installer.py +37 -15
- astrbot/core/utils/session_lock.py +29 -0
- astrbot/core/utils/session_waiter.py +19 -20
- astrbot/core/utils/shared_preferences.py +174 -34
- astrbot/core/utils/t2i/__init__.py +4 -1
- astrbot/core/utils/t2i/local_strategy.py +386 -238
- astrbot/core/utils/t2i/network_strategy.py +109 -49
- astrbot/core/utils/t2i/renderer.py +29 -14
- astrbot/core/utils/t2i/template/astrbot_powershell.html +184 -0
- astrbot/core/utils/t2i/template_manager.py +111 -0
- astrbot/core/utils/tencent_record_helper.py +115 -1
- astrbot/core/utils/version_comparator.py +10 -13
- astrbot/core/zip_updator.py +112 -65
- astrbot/dashboard/routes/__init__.py +20 -13
- astrbot/dashboard/routes/auth.py +20 -9
- astrbot/dashboard/routes/chat.py +297 -141
- astrbot/dashboard/routes/config.py +652 -55
- astrbot/dashboard/routes/conversation.py +107 -37
- astrbot/dashboard/routes/file.py +26 -0
- astrbot/dashboard/routes/knowledge_base.py +1244 -0
- astrbot/dashboard/routes/log.py +27 -2
- astrbot/dashboard/routes/persona.py +202 -0
- astrbot/dashboard/routes/plugin.py +197 -139
- astrbot/dashboard/routes/route.py +27 -7
- astrbot/dashboard/routes/session_management.py +354 -0
- astrbot/dashboard/routes/stat.py +85 -18
- astrbot/dashboard/routes/static_file.py +5 -2
- astrbot/dashboard/routes/t2i.py +233 -0
- astrbot/dashboard/routes/tools.py +184 -120
- astrbot/dashboard/routes/update.py +59 -36
- astrbot/dashboard/server.py +96 -36
- astrbot/dashboard/utils.py +165 -0
- astrbot-4.7.0.dist-info/METADATA +294 -0
- astrbot-4.7.0.dist-info/RECORD +274 -0
- {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/WHEEL +1 -1
- astrbot/core/db/plugin/sqlite_impl.py +0 -112
- astrbot/core/db/sqlite_init.sql +0 -50
- astrbot/core/pipeline/platform_compatibility/stage.py +0 -56
- astrbot/core/pipeline/process_stage/method/llm_request.py +0 -606
- astrbot/core/platform/sources/gewechat/client.py +0 -806
- astrbot/core/platform/sources/gewechat/downloader.py +0 -55
- astrbot/core/platform/sources/gewechat/gewechat_event.py +0 -255
- astrbot/core/platform/sources/gewechat/gewechat_platform_adapter.py +0 -103
- astrbot/core/platform/sources/gewechat/xml_data_parser.py +0 -110
- astrbot/core/provider/sources/dashscope_source.py +0 -203
- astrbot/core/provider/sources/dify_source.py +0 -281
- astrbot/core/provider/sources/llmtuner_source.py +0 -132
- astrbot/core/rag/embedding/openai_source.py +0 -20
- astrbot/core/rag/knowledge_db_mgr.py +0 -94
- astrbot/core/rag/store/__init__.py +0 -9
- astrbot/core/rag/store/chroma_db.py +0 -42
- astrbot/core/utils/dify_api_client.py +0 -152
- astrbot-3.5.6.dist-info/METADATA +0 -249
- astrbot-3.5.6.dist-info/RECORD +0 -158
- {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/entry_points.txt +0 -0
- {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
"""Misskey 平台适配器通用工具函数"""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import astrbot.api.message_components as Comp
|
|
6
|
+
from astrbot.api.platform import AstrBotMessage, MessageMember, MessageType
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FileIDExtractor:
|
|
10
|
+
"""从 API 响应中提取文件 ID 的帮助类(无状态)。"""
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def extract_file_id(result: Any) -> str | None:
|
|
14
|
+
if not isinstance(result, dict):
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
id_paths = [
|
|
18
|
+
lambda r: r.get("createdFile", {}).get("id"),
|
|
19
|
+
lambda r: r.get("file", {}).get("id"),
|
|
20
|
+
lambda r: r.get("id"),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
for p in id_paths:
|
|
24
|
+
try:
|
|
25
|
+
if fid := p(result):
|
|
26
|
+
return fid
|
|
27
|
+
except Exception:
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MessagePayloadBuilder:
|
|
34
|
+
"""构建不同类型消息负载的帮助类(无状态)。"""
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def build_chat_payload(
|
|
38
|
+
user_id: str,
|
|
39
|
+
text: str | None,
|
|
40
|
+
file_id: str | None = None,
|
|
41
|
+
) -> dict[str, Any]:
|
|
42
|
+
payload = {"toUserId": user_id}
|
|
43
|
+
if text:
|
|
44
|
+
payload["text"] = text
|
|
45
|
+
if file_id:
|
|
46
|
+
payload["fileId"] = file_id
|
|
47
|
+
return payload
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def build_room_payload(
|
|
51
|
+
room_id: str,
|
|
52
|
+
text: str | None,
|
|
53
|
+
file_id: str | None = None,
|
|
54
|
+
) -> dict[str, Any]:
|
|
55
|
+
payload = {"toRoomId": room_id}
|
|
56
|
+
if text:
|
|
57
|
+
payload["text"] = text
|
|
58
|
+
if file_id:
|
|
59
|
+
payload["fileId"] = file_id
|
|
60
|
+
return payload
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def build_note_payload(
|
|
64
|
+
text: str | None,
|
|
65
|
+
file_ids: list[str] | None = None,
|
|
66
|
+
**kwargs,
|
|
67
|
+
) -> dict[str, Any]:
|
|
68
|
+
payload: dict[str, Any] = {}
|
|
69
|
+
if text:
|
|
70
|
+
payload["text"] = text
|
|
71
|
+
if file_ids:
|
|
72
|
+
payload["fileIds"] = file_ids
|
|
73
|
+
payload |= kwargs
|
|
74
|
+
return payload
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def serialize_message_chain(chain: list[Any]) -> tuple[str, bool]:
|
|
78
|
+
"""将消息链序列化为文本字符串"""
|
|
79
|
+
text_parts = []
|
|
80
|
+
has_at = False
|
|
81
|
+
|
|
82
|
+
def process_component(component):
|
|
83
|
+
nonlocal has_at
|
|
84
|
+
if isinstance(component, Comp.Plain):
|
|
85
|
+
return component.text
|
|
86
|
+
if isinstance(component, Comp.File):
|
|
87
|
+
# 为文件组件返回占位符,但适配器仍会处理原组件
|
|
88
|
+
return "[文件]"
|
|
89
|
+
if isinstance(component, Comp.Image):
|
|
90
|
+
# 为图片组件返回占位符,但适配器仍会处理原组件
|
|
91
|
+
return "[图片]"
|
|
92
|
+
if isinstance(component, Comp.At):
|
|
93
|
+
has_at = True
|
|
94
|
+
# 优先使用name字段(用户名),如果没有则使用qq字段
|
|
95
|
+
# 这样可以避免在Misskey中生成 @<user_id> 这样的无效提及
|
|
96
|
+
if hasattr(component, "name") and component.name:
|
|
97
|
+
return f"@{component.name}"
|
|
98
|
+
return f"@{component.qq}"
|
|
99
|
+
if hasattr(component, "text"):
|
|
100
|
+
text = getattr(component, "text", "")
|
|
101
|
+
if "@" in text:
|
|
102
|
+
has_at = True
|
|
103
|
+
return text
|
|
104
|
+
return str(component)
|
|
105
|
+
|
|
106
|
+
for component in chain:
|
|
107
|
+
if isinstance(component, Comp.Node) and component.content:
|
|
108
|
+
for node_comp in component.content:
|
|
109
|
+
result = process_component(node_comp)
|
|
110
|
+
if result:
|
|
111
|
+
text_parts.append(result)
|
|
112
|
+
else:
|
|
113
|
+
result = process_component(component)
|
|
114
|
+
if result:
|
|
115
|
+
text_parts.append(result)
|
|
116
|
+
|
|
117
|
+
return "".join(text_parts), has_at
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def resolve_message_visibility(
|
|
121
|
+
user_id: str | None = None,
|
|
122
|
+
user_cache: dict[str, Any] | None = None,
|
|
123
|
+
self_id: str | None = None,
|
|
124
|
+
raw_message: dict[str, Any] | None = None,
|
|
125
|
+
default_visibility: str = "public",
|
|
126
|
+
) -> tuple[str, list[str] | None]:
|
|
127
|
+
"""解析 Misskey 消息的可见性设置
|
|
128
|
+
|
|
129
|
+
可以从 user_cache 或 raw_message 中解析,支持两种调用方式:
|
|
130
|
+
1. 基于 user_cache: resolve_message_visibility(user_id, user_cache, self_id)
|
|
131
|
+
2. 基于 raw_message: resolve_message_visibility(raw_message=raw_message, self_id=self_id)
|
|
132
|
+
"""
|
|
133
|
+
visibility = default_visibility
|
|
134
|
+
visible_user_ids = None
|
|
135
|
+
|
|
136
|
+
# 优先从 user_cache 解析
|
|
137
|
+
if user_id and user_cache:
|
|
138
|
+
user_info = user_cache.get(user_id)
|
|
139
|
+
if user_info:
|
|
140
|
+
original_visibility = user_info.get("visibility", default_visibility)
|
|
141
|
+
if original_visibility == "specified":
|
|
142
|
+
visibility = "specified"
|
|
143
|
+
original_visible_users = user_info.get("visible_user_ids", [])
|
|
144
|
+
users_to_include = [user_id]
|
|
145
|
+
if self_id:
|
|
146
|
+
users_to_include.append(self_id)
|
|
147
|
+
visible_user_ids = list(set(original_visible_users + users_to_include))
|
|
148
|
+
visible_user_ids = [uid for uid in visible_user_ids if uid]
|
|
149
|
+
else:
|
|
150
|
+
visibility = original_visibility
|
|
151
|
+
return visibility, visible_user_ids
|
|
152
|
+
|
|
153
|
+
# 回退到从 raw_message 解析
|
|
154
|
+
if raw_message:
|
|
155
|
+
original_visibility = raw_message.get("visibility", default_visibility)
|
|
156
|
+
if original_visibility == "specified":
|
|
157
|
+
visibility = "specified"
|
|
158
|
+
original_visible_users = raw_message.get("visibleUserIds", [])
|
|
159
|
+
sender_id = raw_message.get("userId", "")
|
|
160
|
+
|
|
161
|
+
users_to_include = []
|
|
162
|
+
if sender_id:
|
|
163
|
+
users_to_include.append(sender_id)
|
|
164
|
+
if self_id:
|
|
165
|
+
users_to_include.append(self_id)
|
|
166
|
+
|
|
167
|
+
visible_user_ids = list(set(original_visible_users + users_to_include))
|
|
168
|
+
visible_user_ids = [uid for uid in visible_user_ids if uid]
|
|
169
|
+
else:
|
|
170
|
+
visibility = original_visibility
|
|
171
|
+
|
|
172
|
+
return visibility, visible_user_ids
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# 保留旧函数名作为向后兼容的别名
|
|
176
|
+
def resolve_visibility_from_raw_message(
|
|
177
|
+
raw_message: dict[str, Any],
|
|
178
|
+
self_id: str | None = None,
|
|
179
|
+
) -> tuple[str, list[str] | None]:
|
|
180
|
+
"""从原始消息数据中解析可见性设置(已弃用,使用 resolve_message_visibility 替代)"""
|
|
181
|
+
return resolve_message_visibility(raw_message=raw_message, self_id=self_id)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def is_valid_user_session_id(session_id: str | Any) -> bool:
|
|
185
|
+
"""检查 session_id 是否是有效的聊天用户 session_id (仅限chat%前缀)"""
|
|
186
|
+
if not isinstance(session_id, str) or "%" not in session_id:
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
parts = session_id.split("%")
|
|
190
|
+
return (
|
|
191
|
+
len(parts) == 2
|
|
192
|
+
and parts[0] == "chat"
|
|
193
|
+
and bool(parts[1])
|
|
194
|
+
and parts[1] != "unknown"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def is_valid_room_session_id(session_id: str | Any) -> bool:
|
|
199
|
+
"""检查 session_id 是否是有效的房间 session_id (仅限room%前缀)"""
|
|
200
|
+
if not isinstance(session_id, str) or "%" not in session_id:
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
parts = session_id.split("%")
|
|
204
|
+
return (
|
|
205
|
+
len(parts) == 2
|
|
206
|
+
and parts[0] == "room"
|
|
207
|
+
and bool(parts[1])
|
|
208
|
+
and parts[1] != "unknown"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def is_valid_chat_session_id(session_id: str | Any) -> bool:
|
|
213
|
+
"""检查 session_id 是否是有效的聊天 session_id (仅限chat%前缀)"""
|
|
214
|
+
if not isinstance(session_id, str) or "%" not in session_id:
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
parts = session_id.split("%")
|
|
218
|
+
return (
|
|
219
|
+
len(parts) == 2
|
|
220
|
+
and parts[0] == "chat"
|
|
221
|
+
and bool(parts[1])
|
|
222
|
+
and parts[1] != "unknown"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def extract_user_id_from_session_id(session_id: str) -> str:
|
|
227
|
+
"""从 session_id 中提取用户 ID"""
|
|
228
|
+
if "%" in session_id:
|
|
229
|
+
parts = session_id.split("%")
|
|
230
|
+
if len(parts) >= 2:
|
|
231
|
+
return parts[1]
|
|
232
|
+
return session_id
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def extract_room_id_from_session_id(session_id: str) -> str:
|
|
236
|
+
"""从 session_id 中提取房间 ID"""
|
|
237
|
+
if "%" in session_id:
|
|
238
|
+
parts = session_id.split("%")
|
|
239
|
+
if len(parts) >= 2 and parts[0] == "room":
|
|
240
|
+
return parts[1]
|
|
241
|
+
return session_id
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def add_at_mention_if_needed(
|
|
245
|
+
text: str,
|
|
246
|
+
user_info: dict[str, Any] | None,
|
|
247
|
+
has_at: bool = False,
|
|
248
|
+
) -> str:
|
|
249
|
+
"""如果需要且没有@用户,则添加@用户
|
|
250
|
+
|
|
251
|
+
注意:仅在有有效的username时才添加@提及,避免使用用户ID
|
|
252
|
+
"""
|
|
253
|
+
if has_at or not user_info:
|
|
254
|
+
return text
|
|
255
|
+
|
|
256
|
+
username = user_info.get("username")
|
|
257
|
+
# 如果没有username,则不添加@提及,返回原文本
|
|
258
|
+
# 这样可以避免生成 @<user_id> 这样的无效提及
|
|
259
|
+
if not username:
|
|
260
|
+
return text
|
|
261
|
+
|
|
262
|
+
mention = f"@{username}"
|
|
263
|
+
if not text.startswith(mention):
|
|
264
|
+
text = f"{mention}\n{text}".strip()
|
|
265
|
+
|
|
266
|
+
return text
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def create_file_component(file_info: dict[str, Any]) -> tuple[Any, str]:
|
|
270
|
+
"""创建文件组件和描述文本"""
|
|
271
|
+
file_url = file_info.get("url", "")
|
|
272
|
+
file_name = file_info.get("name", "未知文件")
|
|
273
|
+
file_type = file_info.get("type", "")
|
|
274
|
+
|
|
275
|
+
if file_type.startswith("image/"):
|
|
276
|
+
return Comp.Image(url=file_url, file=file_name), f"图片[{file_name}]"
|
|
277
|
+
if file_type.startswith("audio/"):
|
|
278
|
+
return Comp.Record(url=file_url, file=file_name), f"音频[{file_name}]"
|
|
279
|
+
if file_type.startswith("video/"):
|
|
280
|
+
return Comp.Video(url=file_url, file=file_name), f"视频[{file_name}]"
|
|
281
|
+
return Comp.File(name=file_name, url=file_url), f"文件[{file_name}]"
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def process_files(
|
|
285
|
+
message: AstrBotMessage,
|
|
286
|
+
files: list,
|
|
287
|
+
include_text_parts: bool = True,
|
|
288
|
+
) -> list:
|
|
289
|
+
"""处理文件列表,添加到消息组件中并返回文本描述"""
|
|
290
|
+
file_parts = []
|
|
291
|
+
for file_info in files:
|
|
292
|
+
component, part_text = create_file_component(file_info)
|
|
293
|
+
message.message.append(component)
|
|
294
|
+
if include_text_parts:
|
|
295
|
+
file_parts.append(part_text)
|
|
296
|
+
return file_parts
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def format_poll(poll: dict[str, Any]) -> str:
|
|
300
|
+
"""将 Misskey 的 poll 对象格式化为可读字符串。"""
|
|
301
|
+
if not poll or not isinstance(poll, dict):
|
|
302
|
+
return ""
|
|
303
|
+
multiple = poll.get("multiple", False)
|
|
304
|
+
choices = poll.get("choices", [])
|
|
305
|
+
text_choices = [
|
|
306
|
+
f"({idx}) {c.get('text', '')} [{c.get('votes', 0)}票]"
|
|
307
|
+
for idx, c in enumerate(choices, start=1)
|
|
308
|
+
]
|
|
309
|
+
parts = ["[投票]", ("允许多选" if multiple else "单选")] + (
|
|
310
|
+
["选项: " + ", ".join(text_choices)] if text_choices else []
|
|
311
|
+
)
|
|
312
|
+
return " ".join(parts)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def extract_sender_info(
|
|
316
|
+
raw_data: dict[str, Any],
|
|
317
|
+
is_chat: bool = False,
|
|
318
|
+
) -> dict[str, Any]:
|
|
319
|
+
"""提取发送者信息"""
|
|
320
|
+
if is_chat:
|
|
321
|
+
sender = raw_data.get("fromUser", {})
|
|
322
|
+
sender_id = str(sender.get("id", "") or raw_data.get("fromUserId", ""))
|
|
323
|
+
else:
|
|
324
|
+
sender = raw_data.get("user", {})
|
|
325
|
+
sender_id = str(sender.get("id", ""))
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
"sender": sender,
|
|
329
|
+
"sender_id": sender_id,
|
|
330
|
+
"nickname": sender.get("name", sender.get("username", "")),
|
|
331
|
+
"username": sender.get("username", ""),
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def create_base_message(
|
|
336
|
+
raw_data: dict[str, Any],
|
|
337
|
+
sender_info: dict[str, Any],
|
|
338
|
+
client_self_id: str,
|
|
339
|
+
is_chat: bool = False,
|
|
340
|
+
room_id: str | None = None,
|
|
341
|
+
unique_session: bool = False,
|
|
342
|
+
) -> AstrBotMessage:
|
|
343
|
+
"""创建基础消息对象"""
|
|
344
|
+
message = AstrBotMessage()
|
|
345
|
+
message.raw_message = raw_data
|
|
346
|
+
message.message = []
|
|
347
|
+
|
|
348
|
+
message.sender = MessageMember(
|
|
349
|
+
user_id=sender_info["sender_id"],
|
|
350
|
+
nickname=sender_info["nickname"],
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
if room_id:
|
|
354
|
+
session_prefix = "room"
|
|
355
|
+
session_id = f"{session_prefix}%{room_id}"
|
|
356
|
+
if unique_session:
|
|
357
|
+
session_id += f"_{sender_info['sender_id']}"
|
|
358
|
+
message.type = MessageType.GROUP_MESSAGE
|
|
359
|
+
message.group_id = room_id
|
|
360
|
+
elif is_chat:
|
|
361
|
+
session_prefix = "chat"
|
|
362
|
+
session_id = f"{session_prefix}%{sender_info['sender_id']}"
|
|
363
|
+
message.type = MessageType.FRIEND_MESSAGE
|
|
364
|
+
else:
|
|
365
|
+
session_prefix = "note"
|
|
366
|
+
session_id = f"{session_prefix}%{sender_info['sender_id']}"
|
|
367
|
+
message.type = MessageType.OTHER_MESSAGE
|
|
368
|
+
|
|
369
|
+
message.session_id = (
|
|
370
|
+
session_id if sender_info["sender_id"] else f"{session_prefix}%unknown"
|
|
371
|
+
)
|
|
372
|
+
message.message_id = str(raw_data.get("id", ""))
|
|
373
|
+
message.self_id = client_self_id
|
|
374
|
+
|
|
375
|
+
return message
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def process_at_mention(
|
|
379
|
+
message: AstrBotMessage,
|
|
380
|
+
raw_text: str,
|
|
381
|
+
bot_username: str,
|
|
382
|
+
client_self_id: str,
|
|
383
|
+
) -> tuple[list[str], str]:
|
|
384
|
+
"""处理@提及逻辑,返回消息部分列表和处理后的文本"""
|
|
385
|
+
message_parts = []
|
|
386
|
+
|
|
387
|
+
if not raw_text:
|
|
388
|
+
return message_parts, ""
|
|
389
|
+
|
|
390
|
+
if bot_username and raw_text.startswith(f"@{bot_username}"):
|
|
391
|
+
at_mention = f"@{bot_username}"
|
|
392
|
+
message.message.append(Comp.At(qq=client_self_id))
|
|
393
|
+
remaining_text = raw_text[len(at_mention) :].strip()
|
|
394
|
+
if remaining_text:
|
|
395
|
+
message.message.append(Comp.Plain(remaining_text))
|
|
396
|
+
message_parts.append(remaining_text)
|
|
397
|
+
return message_parts, remaining_text
|
|
398
|
+
message.message.append(Comp.Plain(raw_text))
|
|
399
|
+
message_parts.append(raw_text)
|
|
400
|
+
return message_parts, raw_text
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def cache_user_info(
|
|
404
|
+
user_cache: dict[str, Any],
|
|
405
|
+
sender_info: dict[str, Any],
|
|
406
|
+
raw_data: dict[str, Any],
|
|
407
|
+
client_self_id: str,
|
|
408
|
+
is_chat: bool = False,
|
|
409
|
+
):
|
|
410
|
+
"""缓存用户信息"""
|
|
411
|
+
if is_chat:
|
|
412
|
+
user_cache_data = {
|
|
413
|
+
"username": sender_info["username"],
|
|
414
|
+
"nickname": sender_info["nickname"],
|
|
415
|
+
"visibility": "specified",
|
|
416
|
+
"visible_user_ids": [client_self_id, sender_info["sender_id"]],
|
|
417
|
+
}
|
|
418
|
+
else:
|
|
419
|
+
user_cache_data = {
|
|
420
|
+
"username": sender_info["username"],
|
|
421
|
+
"nickname": sender_info["nickname"],
|
|
422
|
+
"visibility": raw_data.get("visibility", "public"),
|
|
423
|
+
"visible_user_ids": raw_data.get("visibleUserIds", []),
|
|
424
|
+
# 保存原消息ID,用于回复时作为reply_id
|
|
425
|
+
"reply_to_note_id": raw_data.get("id"),
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
user_cache[sender_info["sender_id"]] = user_cache_data
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def cache_room_info(
|
|
432
|
+
user_cache: dict[str, Any],
|
|
433
|
+
raw_data: dict[str, Any],
|
|
434
|
+
client_self_id: str,
|
|
435
|
+
):
|
|
436
|
+
"""缓存房间信息"""
|
|
437
|
+
room_data = raw_data.get("toRoom")
|
|
438
|
+
room_id = raw_data.get("toRoomId")
|
|
439
|
+
|
|
440
|
+
if room_data and room_id:
|
|
441
|
+
room_cache_key = f"room:{room_id}"
|
|
442
|
+
user_cache[room_cache_key] = {
|
|
443
|
+
"room_id": room_id,
|
|
444
|
+
"room_name": room_data.get("name", ""),
|
|
445
|
+
"room_description": room_data.get("description", ""),
|
|
446
|
+
"owner_id": room_data.get("ownerId", ""),
|
|
447
|
+
"visibility": "specified",
|
|
448
|
+
"visible_user_ids": [client_self_id],
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
async def resolve_component_url_or_path(
|
|
453
|
+
comp: Any,
|
|
454
|
+
) -> tuple[str | None, str | None]:
|
|
455
|
+
"""尝试从组件解析可上传的远程 URL 或本地路径。
|
|
456
|
+
|
|
457
|
+
返回 (url_candidate, local_path)。两者可能都为 None。
|
|
458
|
+
这个函数尽量不抛异常,调用方可按需处理 None。
|
|
459
|
+
"""
|
|
460
|
+
url_candidate = None
|
|
461
|
+
local_path = None
|
|
462
|
+
|
|
463
|
+
async def _get_str_value(coro_or_val):
|
|
464
|
+
"""辅助函数:统一处理协程或普通值"""
|
|
465
|
+
try:
|
|
466
|
+
if hasattr(coro_or_val, "__await__"):
|
|
467
|
+
result = await coro_or_val
|
|
468
|
+
else:
|
|
469
|
+
result = coro_or_val
|
|
470
|
+
return result if isinstance(result, str) else None
|
|
471
|
+
except Exception:
|
|
472
|
+
return None
|
|
473
|
+
|
|
474
|
+
try:
|
|
475
|
+
# 1. 尝试异步方法
|
|
476
|
+
for method in ["convert_to_file_path", "get_file", "register_to_file_service"]:
|
|
477
|
+
if not hasattr(comp, method):
|
|
478
|
+
continue
|
|
479
|
+
try:
|
|
480
|
+
value = await _get_str_value(getattr(comp, method)())
|
|
481
|
+
if value:
|
|
482
|
+
if value.startswith("http"):
|
|
483
|
+
url_candidate = value
|
|
484
|
+
break
|
|
485
|
+
local_path = value
|
|
486
|
+
except Exception:
|
|
487
|
+
continue
|
|
488
|
+
|
|
489
|
+
# 2. 尝试 get_file(True) 获取可直接访问的 URL
|
|
490
|
+
if not url_candidate and hasattr(comp, "get_file"):
|
|
491
|
+
try:
|
|
492
|
+
value = await _get_str_value(comp.get_file(True))
|
|
493
|
+
if value and value.startswith("http"):
|
|
494
|
+
url_candidate = value
|
|
495
|
+
except Exception:
|
|
496
|
+
pass
|
|
497
|
+
|
|
498
|
+
# 3. 回退到同步属性
|
|
499
|
+
if not url_candidate and not local_path:
|
|
500
|
+
for attr in ("file", "url", "path", "src", "source"):
|
|
501
|
+
try:
|
|
502
|
+
value = getattr(comp, attr, None)
|
|
503
|
+
if value and isinstance(value, str):
|
|
504
|
+
if value.startswith("http"):
|
|
505
|
+
url_candidate = value
|
|
506
|
+
break
|
|
507
|
+
local_path = value
|
|
508
|
+
break
|
|
509
|
+
except Exception:
|
|
510
|
+
continue
|
|
511
|
+
|
|
512
|
+
except Exception:
|
|
513
|
+
pass
|
|
514
|
+
|
|
515
|
+
return url_candidate, local_path
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def summarize_component_for_log(comp: Any) -> dict[str, Any]:
|
|
519
|
+
"""生成适合日志的组件属性字典(尽量不抛异常)。"""
|
|
520
|
+
attrs = {}
|
|
521
|
+
for a in ("file", "url", "path", "src", "source", "name"):
|
|
522
|
+
try:
|
|
523
|
+
v = getattr(comp, a, None)
|
|
524
|
+
if v is not None:
|
|
525
|
+
attrs[a] = v
|
|
526
|
+
except Exception:
|
|
527
|
+
continue
|
|
528
|
+
return attrs
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
async def upload_local_with_retries(
|
|
532
|
+
api: Any,
|
|
533
|
+
local_path: str,
|
|
534
|
+
preferred_name: str | None,
|
|
535
|
+
folder_id: str | None,
|
|
536
|
+
) -> str | None:
|
|
537
|
+
"""尝试本地上传,返回 file id 或 None。如果文件类型不允许则直接失败。"""
|
|
538
|
+
try:
|
|
539
|
+
res = await api.upload_file(local_path, preferred_name, folder_id)
|
|
540
|
+
if isinstance(res, dict):
|
|
541
|
+
fid = res.get("id") or (res.get("raw") or {}).get("createdFile", {}).get(
|
|
542
|
+
"id",
|
|
543
|
+
)
|
|
544
|
+
if fid:
|
|
545
|
+
return str(fid)
|
|
546
|
+
except Exception:
|
|
547
|
+
# 上传失败,直接返回 None,让上层处理错误
|
|
548
|
+
return None
|
|
549
|
+
|
|
550
|
+
return None
|