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
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import traceback
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
from quart import request
|
|
4
|
+
|
|
5
|
+
from astrbot.core import DEMO_MODE, logger, pip_installer
|
|
6
|
+
from astrbot.core.config.default import VERSION
|
|
4
7
|
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
|
8
|
+
from astrbot.core.db.migration.helper import check_migration_needed_v4, do_migration_v4
|
|
5
9
|
from astrbot.core.updator import AstrBotUpdator
|
|
6
|
-
from astrbot.core import logger, pip_installer
|
|
7
10
|
from astrbot.core.utils.io import download_dashboard, get_dashboard_version
|
|
8
|
-
|
|
9
|
-
from
|
|
11
|
+
|
|
12
|
+
from .route import Response, Route, RouteContext
|
|
13
|
+
|
|
14
|
+
CLEAR_SITE_DATA_HEADERS = {"Clear-Site-Data": '"cache"'}
|
|
10
15
|
|
|
11
16
|
|
|
12
17
|
class UpdateRoute(Route):
|
|
@@ -23,11 +28,29 @@ class UpdateRoute(Route):
|
|
|
23
28
|
"/update/do": ("POST", self.update_project),
|
|
24
29
|
"/update/dashboard": ("POST", self.update_dashboard),
|
|
25
30
|
"/update/pip-install": ("POST", self.install_pip_package),
|
|
31
|
+
"/update/migration": ("POST", self.do_migration),
|
|
26
32
|
}
|
|
27
33
|
self.astrbot_updator = astrbot_updator
|
|
28
34
|
self.core_lifecycle = core_lifecycle
|
|
29
35
|
self.register_routes()
|
|
30
36
|
|
|
37
|
+
async def do_migration(self):
|
|
38
|
+
need_migration = await check_migration_needed_v4(self.core_lifecycle.db)
|
|
39
|
+
if not need_migration:
|
|
40
|
+
return Response().ok(None, "不需要进行迁移。").__dict__
|
|
41
|
+
try:
|
|
42
|
+
data = await request.json
|
|
43
|
+
pim = data.get("platform_id_map", {})
|
|
44
|
+
await do_migration_v4(
|
|
45
|
+
self.core_lifecycle.db,
|
|
46
|
+
pim,
|
|
47
|
+
self.core_lifecycle.astrbot_config,
|
|
48
|
+
)
|
|
49
|
+
return Response().ok(None, "迁移成功。").__dict__
|
|
50
|
+
except Exception as e:
|
|
51
|
+
logger.error(f"迁移失败: {traceback.format_exc()}")
|
|
52
|
+
return Response().error(f"迁移失败: {e!s}").__dict__
|
|
53
|
+
|
|
31
54
|
async def check_update(self):
|
|
32
55
|
type_ = request.args.get("type", None)
|
|
33
56
|
|
|
@@ -39,20 +62,19 @@ class UpdateRoute(Route):
|
|
|
39
62
|
.ok({"has_new_version": dv != f"v{VERSION}", "current_version": dv})
|
|
40
63
|
.__dict__
|
|
41
64
|
)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
).__dict__
|
|
65
|
+
ret = await self.astrbot_updator.check_update(None, None, False)
|
|
66
|
+
return Response(
|
|
67
|
+
status="success",
|
|
68
|
+
message=str(ret) if ret is not None else "已经是最新版本了。",
|
|
69
|
+
data={
|
|
70
|
+
"version": f"v{VERSION}",
|
|
71
|
+
"has_new_version": ret is not None,
|
|
72
|
+
"dashboard_version": dv,
|
|
73
|
+
"dashboard_has_new_version": bool(dv and dv != f"v{VERSION}"),
|
|
74
|
+
},
|
|
75
|
+
).__dict__
|
|
54
76
|
except Exception as e:
|
|
55
|
-
logger.warning(f"检查更新失败: {
|
|
77
|
+
logger.warning(f"检查更新失败: {e!s} (不影响除项目更新外的正常使用)")
|
|
56
78
|
return Response().error(e.__str__()).__dict__
|
|
57
79
|
|
|
58
80
|
async def get_releases(self):
|
|
@@ -79,35 +101,37 @@ class UpdateRoute(Route):
|
|
|
79
101
|
|
|
80
102
|
try:
|
|
81
103
|
await self.astrbot_updator.update(
|
|
82
|
-
latest=latest,
|
|
104
|
+
latest=latest,
|
|
105
|
+
version=version,
|
|
106
|
+
proxy=proxy,
|
|
83
107
|
)
|
|
84
108
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
logger.error(f"下载管理面板文件失败: {e}。")
|
|
109
|
+
try:
|
|
110
|
+
await download_dashboard(latest=latest, version=version, proxy=proxy)
|
|
111
|
+
except Exception as e:
|
|
112
|
+
logger.error(f"下载管理面板文件失败: {e}。")
|
|
90
113
|
|
|
91
114
|
# pip 更新依赖
|
|
92
115
|
logger.info("更新依赖中...")
|
|
93
116
|
try:
|
|
94
|
-
pip_installer.install(requirements_path="requirements.txt")
|
|
117
|
+
await pip_installer.install(requirements_path="requirements.txt")
|
|
95
118
|
except Exception as e:
|
|
96
119
|
logger.error(f"更新依赖失败: {e}")
|
|
97
120
|
|
|
98
121
|
if reboot:
|
|
99
122
|
await self.core_lifecycle.restart()
|
|
100
|
-
|
|
123
|
+
ret = (
|
|
101
124
|
Response()
|
|
102
125
|
.ok(None, "更新成功,AstrBot 将在 2 秒内全量重启以应用新的代码。")
|
|
103
126
|
.__dict__
|
|
104
127
|
)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
128
|
+
return ret, 200, CLEAR_SITE_DATA_HEADERS
|
|
129
|
+
ret = (
|
|
130
|
+
Response()
|
|
131
|
+
.ok(None, "更新成功,AstrBot 将在下次启动时应用新的代码。")
|
|
132
|
+
.__dict__
|
|
133
|
+
)
|
|
134
|
+
return ret, 200, CLEAR_SITE_DATA_HEADERS
|
|
111
135
|
except Exception as e:
|
|
112
136
|
logger.error(f"/api/update_project: {traceback.format_exc()}")
|
|
113
137
|
return Response().error(e.__str__()).__dict__
|
|
@@ -115,13 +139,12 @@ class UpdateRoute(Route):
|
|
|
115
139
|
async def update_dashboard(self):
|
|
116
140
|
try:
|
|
117
141
|
try:
|
|
118
|
-
await download_dashboard()
|
|
142
|
+
await download_dashboard(version=f"v{VERSION}", latest=False)
|
|
119
143
|
except Exception as e:
|
|
120
144
|
logger.error(f"下载管理面板文件失败: {e}。")
|
|
121
145
|
return Response().error(f"下载管理面板文件失败: {e}").__dict__
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
)
|
|
146
|
+
ret = Response().ok(None, "更新成功。刷新页面即可应用新版本面板。").__dict__
|
|
147
|
+
return ret, 200, CLEAR_SITE_DATA_HEADERS
|
|
125
148
|
except Exception as e:
|
|
126
149
|
logger.error(f"/api/update_dashboard: {traceback.format_exc()}")
|
|
127
150
|
return Response().error(e.__str__()).__dict__
|
|
@@ -140,7 +163,7 @@ class UpdateRoute(Route):
|
|
|
140
163
|
if not package:
|
|
141
164
|
return Response().error("缺少参数 package 或不合法。").__dict__
|
|
142
165
|
try:
|
|
143
|
-
pip_installer.install(package, mirror=mirror)
|
|
166
|
+
await pip_installer.install(package, mirror=mirror)
|
|
144
167
|
return Response().ok(None, "安装成功。").__dict__
|
|
145
168
|
except Exception as e:
|
|
146
169
|
logger.error(f"/api/update_pip: {traceback.format_exc()}")
|
astrbot/dashboard/server.py
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import jwt
|
|
3
1
|
import asyncio
|
|
2
|
+
import logging
|
|
4
3
|
import os
|
|
5
4
|
import socket
|
|
5
|
+
|
|
6
|
+
import jwt
|
|
6
7
|
import psutil
|
|
7
|
-
from
|
|
8
|
-
from quart import Quart, request, jsonify, g
|
|
8
|
+
from quart import Quart, g, jsonify, request
|
|
9
9
|
from quart.logging import default_handler
|
|
10
|
+
|
|
11
|
+
from astrbot.core import logger
|
|
12
|
+
from astrbot.core.config.default import VERSION
|
|
10
13
|
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
|
11
|
-
from .routes import *
|
|
12
|
-
from .routes.route import RouteContext, Response
|
|
13
|
-
from astrbot.core import logger, WEBUI_SK
|
|
14
14
|
from astrbot.core.db import BaseDatabase
|
|
15
|
+
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
|
15
16
|
from astrbot.core.utils.io import get_local_ip_addresses
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
from .routes import *
|
|
19
|
+
from .routes.route import Response, RouteContext
|
|
20
|
+
from .routes.session_management import SessionManagementRoute
|
|
21
|
+
from .routes.t2i import T2iRoute
|
|
22
|
+
|
|
23
|
+
APP: Quart = None
|
|
20
24
|
|
|
21
25
|
|
|
22
26
|
class AstrBotDashboard:
|
|
@@ -25,11 +29,21 @@ class AstrBotDashboard:
|
|
|
25
29
|
core_lifecycle: AstrBotCoreLifecycle,
|
|
26
30
|
db: BaseDatabase,
|
|
27
31
|
shutdown_event: asyncio.Event,
|
|
32
|
+
webui_dir: str | None = None,
|
|
28
33
|
) -> None:
|
|
29
34
|
self.core_lifecycle = core_lifecycle
|
|
30
35
|
self.config = core_lifecycle.astrbot_config
|
|
31
|
-
|
|
36
|
+
|
|
37
|
+
# 参数指定webui目录
|
|
38
|
+
if webui_dir and os.path.exists(webui_dir):
|
|
39
|
+
self.data_path = os.path.abspath(webui_dir)
|
|
40
|
+
else:
|
|
41
|
+
self.data_path = os.path.abspath(
|
|
42
|
+
os.path.join(get_astrbot_data_path(), "dist"),
|
|
43
|
+
)
|
|
44
|
+
|
|
32
45
|
self.app = Quart("dashboard", static_folder=self.data_path, static_url_path="/")
|
|
46
|
+
APP = self.app # noqa
|
|
33
47
|
self.app.config["MAX_CONTENT_LENGTH"] = (
|
|
34
48
|
128 * 1024 * 1024
|
|
35
49
|
) # 将 Flask 允许的最大上传文件体大小设置为 128 MB
|
|
@@ -39,11 +53,15 @@ class AstrBotDashboard:
|
|
|
39
53
|
logging.getLogger(self.app.name).removeHandler(default_handler)
|
|
40
54
|
self.context = RouteContext(self.config, self.app)
|
|
41
55
|
self.ur = UpdateRoute(
|
|
42
|
-
self.context,
|
|
56
|
+
self.context,
|
|
57
|
+
core_lifecycle.astrbot_updator,
|
|
58
|
+
core_lifecycle,
|
|
43
59
|
)
|
|
44
60
|
self.sr = StatRoute(self.context, db, core_lifecycle)
|
|
45
61
|
self.pr = PluginRoute(
|
|
46
|
-
self.context,
|
|
62
|
+
self.context,
|
|
63
|
+
core_lifecycle,
|
|
64
|
+
core_lifecycle.plugin_manager,
|
|
47
65
|
)
|
|
48
66
|
self.cr = ConfigRoute(self.context, core_lifecycle)
|
|
49
67
|
self.lr = LogRoute(self.context, core_lifecycle.log_broker)
|
|
@@ -52,26 +70,50 @@ class AstrBotDashboard:
|
|
|
52
70
|
self.chat_route = ChatRoute(self.context, db, core_lifecycle)
|
|
53
71
|
self.tools_root = ToolsRoute(self.context, core_lifecycle)
|
|
54
72
|
self.conversation_route = ConversationRoute(self.context, db, core_lifecycle)
|
|
73
|
+
self.file_route = FileRoute(self.context)
|
|
74
|
+
self.session_management_route = SessionManagementRoute(
|
|
75
|
+
self.context,
|
|
76
|
+
db,
|
|
77
|
+
core_lifecycle,
|
|
78
|
+
)
|
|
79
|
+
self.persona_route = PersonaRoute(self.context, db, core_lifecycle)
|
|
80
|
+
self.t2i_route = T2iRoute(self.context, core_lifecycle)
|
|
81
|
+
self.kb_route = KnowledgeBaseRoute(self.context, core_lifecycle)
|
|
82
|
+
|
|
83
|
+
self.app.add_url_rule(
|
|
84
|
+
"/api/plug/<path:subpath>",
|
|
85
|
+
view_func=self.srv_plug_route,
|
|
86
|
+
methods=["GET", "POST"],
|
|
87
|
+
)
|
|
55
88
|
|
|
56
89
|
self.shutdown_event = shutdown_event
|
|
57
90
|
|
|
91
|
+
self._init_jwt_secret()
|
|
92
|
+
|
|
93
|
+
async def srv_plug_route(self, subpath, *args, **kwargs):
|
|
94
|
+
"""插件路由"""
|
|
95
|
+
registered_web_apis = self.core_lifecycle.star_context.registered_web_apis
|
|
96
|
+
for api in registered_web_apis:
|
|
97
|
+
route, view_handler, methods, _ = api
|
|
98
|
+
if route == f"/{subpath}" and request.method in methods:
|
|
99
|
+
return await view_handler(*args, **kwargs)
|
|
100
|
+
return jsonify(Response().error("未找到该路由").__dict__)
|
|
101
|
+
|
|
58
102
|
async def auth_middleware(self):
|
|
59
103
|
if not request.path.startswith("/api"):
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
# claim jwt
|
|
104
|
+
return None
|
|
105
|
+
allowed_endpoints = ["/api/auth/login", "/api/file"]
|
|
106
|
+
if any(request.path.startswith(prefix) for prefix in allowed_endpoints):
|
|
107
|
+
return None
|
|
108
|
+
# 声明 JWT
|
|
66
109
|
token = request.headers.get("Authorization")
|
|
67
110
|
if not token:
|
|
68
111
|
r = jsonify(Response().error("未授权").__dict__)
|
|
69
112
|
r.status_code = 401
|
|
70
113
|
return r
|
|
71
|
-
|
|
72
|
-
token = token[7:]
|
|
114
|
+
token = token.removeprefix("Bearer ")
|
|
73
115
|
try:
|
|
74
|
-
payload = jwt.decode(token,
|
|
116
|
+
payload = jwt.decode(token, self._jwt_secret, algorithms=["HS256"])
|
|
75
117
|
g.username = payload["username"]
|
|
76
118
|
except jwt.ExpiredSignatureError:
|
|
77
119
|
r = jsonify(Response().error("Token 过期").__dict__)
|
|
@@ -83,9 +125,7 @@ class AstrBotDashboard:
|
|
|
83
125
|
return r
|
|
84
126
|
|
|
85
127
|
def check_port_in_use(self, port: int) -> bool:
|
|
86
|
-
"""
|
|
87
|
-
跨平台检测端口是否被占用
|
|
88
|
-
"""
|
|
128
|
+
"""跨平台检测端口是否被占用"""
|
|
89
129
|
try:
|
|
90
130
|
# 创建 IPv4 TCP Socket
|
|
91
131
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
@@ -97,7 +137,7 @@ class AstrBotDashboard:
|
|
|
97
137
|
# result 为 0 表示端口被占用
|
|
98
138
|
return result == 0
|
|
99
139
|
except Exception as e:
|
|
100
|
-
logger.warning(f"检查端口 {port} 时发生错误: {
|
|
140
|
+
logger.warning(f"检查端口 {port} 时发生错误: {e!s}")
|
|
101
141
|
# 如果出现异常,保守起见认为端口可能被占用
|
|
102
142
|
return True
|
|
103
143
|
|
|
@@ -118,21 +158,38 @@ class AstrBotDashboard:
|
|
|
118
158
|
]
|
|
119
159
|
return "\n ".join(proc_info)
|
|
120
160
|
except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
|
|
121
|
-
return f"无法获取进程详细信息(可能需要管理员权限): {
|
|
161
|
+
return f"无法获取进程详细信息(可能需要管理员权限): {e!s}"
|
|
122
162
|
return "未找到占用进程"
|
|
123
163
|
except Exception as e:
|
|
124
|
-
return f"获取进程信息失败: {
|
|
164
|
+
return f"获取进程信息失败: {e!s}"
|
|
165
|
+
|
|
166
|
+
def _init_jwt_secret(self):
|
|
167
|
+
if not self.config.get("dashboard", {}).get("jwt_secret", None):
|
|
168
|
+
# 如果没有设置 JWT 密钥,则生成一个新的密钥
|
|
169
|
+
jwt_secret = os.urandom(32).hex()
|
|
170
|
+
self.config["dashboard"]["jwt_secret"] = jwt_secret
|
|
171
|
+
self.config.save_config()
|
|
172
|
+
logger.info("Initialized random JWT secret for dashboard.")
|
|
173
|
+
self._jwt_secret = self.config["dashboard"]["jwt_secret"]
|
|
125
174
|
|
|
126
175
|
def run(self):
|
|
127
176
|
ip_addr = []
|
|
128
|
-
|
|
177
|
+
if p := os.environ.get("DASHBOARD_PORT"):
|
|
178
|
+
port = p
|
|
179
|
+
else:
|
|
180
|
+
port = self.core_lifecycle.astrbot_config["dashboard"].get("port", 6185)
|
|
129
181
|
host = self.core_lifecycle.astrbot_config["dashboard"].get("host", "0.0.0.0")
|
|
182
|
+
enable = self.core_lifecycle.astrbot_config["dashboard"].get("enable", True)
|
|
183
|
+
|
|
184
|
+
if not enable:
|
|
185
|
+
logger.info("WebUI 已被禁用")
|
|
186
|
+
return None
|
|
130
187
|
|
|
131
188
|
logger.info(f"正在启动 WebUI, 监听地址: http://{host}:{port}")
|
|
132
189
|
|
|
133
190
|
if host == "0.0.0.0":
|
|
134
191
|
logger.info(
|
|
135
|
-
"提示: WebUI 将监听所有网络接口,请注意安全。(可在 data/cmd_config.json 中配置 dashboard.host 以修改 host)"
|
|
192
|
+
"提示: WebUI 将监听所有网络接口,请注意安全。(可在 data/cmd_config.json 中配置 dashboard.host 以修改 host)",
|
|
136
193
|
)
|
|
137
194
|
|
|
138
195
|
if host not in ["localhost", "127.0.0.1"]:
|
|
@@ -151,16 +208,17 @@ class AstrBotDashboard:
|
|
|
151
208
|
f"请确保:\n"
|
|
152
209
|
f"1. 没有其他 AstrBot 实例正在运行\n"
|
|
153
210
|
f"2. 端口 {port} 没有被其他程序占用\n"
|
|
154
|
-
f"3. 如需使用其他端口,请修改配置文件"
|
|
211
|
+
f"3. 如需使用其他端口,请修改配置文件",
|
|
155
212
|
)
|
|
156
213
|
|
|
157
214
|
raise Exception(f"端口 {port} 已被占用")
|
|
158
215
|
|
|
159
|
-
|
|
160
|
-
|
|
216
|
+
parts = [f"\n ✨✨✨\n AstrBot v{VERSION} WebUI 已启动,可访问\n\n"]
|
|
217
|
+
parts.append(f" ➜ 本地: http://localhost:{port}\n")
|
|
161
218
|
for ip in ip_addr:
|
|
162
|
-
|
|
163
|
-
|
|
219
|
+
parts.append(f" ➜ 网络: http://{ip}:{port}\n")
|
|
220
|
+
parts.append(" ➜ 默认用户名和密码: astrbot\n ✨✨✨\n")
|
|
221
|
+
display = "".join(parts)
|
|
164
222
|
|
|
165
223
|
if not ip_addr:
|
|
166
224
|
display += (
|
|
@@ -170,7 +228,9 @@ class AstrBotDashboard:
|
|
|
170
228
|
logger.info(display)
|
|
171
229
|
|
|
172
230
|
return self.app.run_task(
|
|
173
|
-
host=host,
|
|
231
|
+
host=host,
|
|
232
|
+
port=port,
|
|
233
|
+
shutdown_trigger=self.shutdown_trigger,
|
|
174
234
|
)
|
|
175
235
|
|
|
176
236
|
async def shutdown_trigger(self):
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import os
|
|
3
|
+
import traceback
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
|
|
6
|
+
from astrbot.api import logger
|
|
7
|
+
from astrbot.core.db.vec_db.faiss_impl import FaissVecDB
|
|
8
|
+
from astrbot.core.knowledge_base.kb_helper import KBHelper
|
|
9
|
+
from astrbot.core.knowledge_base.kb_mgr import KnowledgeBaseManager
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def generate_tsne_visualization(
|
|
13
|
+
query: str,
|
|
14
|
+
kb_names: list[str],
|
|
15
|
+
kb_manager: KnowledgeBaseManager,
|
|
16
|
+
) -> str | None:
|
|
17
|
+
"""生成 t-SNE 可视化图片
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
query: 查询文本
|
|
21
|
+
kb_names: 知识库名称列表
|
|
22
|
+
kb_manager: 知识库管理器
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
图片路径或 None
|
|
26
|
+
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
import faiss
|
|
30
|
+
import matplotlib
|
|
31
|
+
import numpy as np
|
|
32
|
+
|
|
33
|
+
matplotlib.use("Agg") # 使用非交互式后端
|
|
34
|
+
import matplotlib.pyplot as plt
|
|
35
|
+
from sklearn.manifold import TSNE
|
|
36
|
+
except ImportError as e:
|
|
37
|
+
raise Exception(
|
|
38
|
+
"缺少必要的库以生成 t-SNE 可视化。请安装 matplotlib 和 scikit-learn: {e}",
|
|
39
|
+
) from e
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
# 获取第一个知识库的向量数据
|
|
43
|
+
kb_helper: KBHelper | None = None
|
|
44
|
+
for kb_name in kb_names:
|
|
45
|
+
kb_helper = await kb_manager.get_kb_by_name(kb_name)
|
|
46
|
+
if kb_helper:
|
|
47
|
+
break
|
|
48
|
+
|
|
49
|
+
if not kb_helper:
|
|
50
|
+
logger.warning("未找到知识库")
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
kb = kb_helper.kb
|
|
54
|
+
index_path = f"data/knowledge_base/{kb.kb_id}/index.faiss"
|
|
55
|
+
|
|
56
|
+
# 读取 FAISS 索引
|
|
57
|
+
if not os.path.exists(index_path):
|
|
58
|
+
logger.warning(f"FAISS 索引不存在: {index_path}")
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
index = faiss.read_index(index_path)
|
|
62
|
+
|
|
63
|
+
if index.ntotal == 0:
|
|
64
|
+
logger.warning("索引为空")
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
# 提取所有向量
|
|
68
|
+
logger.info(f"提取 {index.ntotal} 个向量用于可视化...")
|
|
69
|
+
if isinstance(index, faiss.IndexIDMap):
|
|
70
|
+
base_index = faiss.downcast_index(index.index)
|
|
71
|
+
if hasattr(base_index, "reconstruct_n"):
|
|
72
|
+
vectors = base_index.reconstruct_n(0, index.ntotal)
|
|
73
|
+
else:
|
|
74
|
+
vectors = np.zeros((index.ntotal, index.d), dtype=np.float32)
|
|
75
|
+
for i in range(index.ntotal):
|
|
76
|
+
base_index.reconstruct(i, vectors[i])
|
|
77
|
+
elif hasattr(index, "reconstruct_n"):
|
|
78
|
+
vectors = index.reconstruct_n(0, index.ntotal)
|
|
79
|
+
else:
|
|
80
|
+
vectors = np.zeros((index.ntotal, index.d), dtype=np.float32)
|
|
81
|
+
for i in range(index.ntotal):
|
|
82
|
+
index.reconstruct(i, vectors[i])
|
|
83
|
+
|
|
84
|
+
# 获取查询向量
|
|
85
|
+
vec_db: FaissVecDB = kb_helper.vec_db # type: ignore
|
|
86
|
+
embedding_provider = vec_db.embedding_provider
|
|
87
|
+
query_embedding = await embedding_provider.get_embedding(query)
|
|
88
|
+
query_vector = np.array([query_embedding], dtype=np.float32)
|
|
89
|
+
|
|
90
|
+
# 合并所有向量和查询向量
|
|
91
|
+
all_vectors = np.vstack([vectors, query_vector])
|
|
92
|
+
|
|
93
|
+
# t-SNE 降维
|
|
94
|
+
logger.info("开始 t-SNE 降维...")
|
|
95
|
+
perplexity = min(30, all_vectors.shape[0] - 1)
|
|
96
|
+
tsne = TSNE(n_components=2, random_state=42, perplexity=perplexity)
|
|
97
|
+
vectors_2d = tsne.fit_transform(all_vectors)
|
|
98
|
+
|
|
99
|
+
# 分离知识库向量和查询向量
|
|
100
|
+
kb_vectors_2d = vectors_2d[:-1]
|
|
101
|
+
query_vector_2d = vectors_2d[-1]
|
|
102
|
+
|
|
103
|
+
# 可视化
|
|
104
|
+
logger.info("生成可视化图表...")
|
|
105
|
+
plt.figure(figsize=(14, 10))
|
|
106
|
+
|
|
107
|
+
# 绘制知识库向量
|
|
108
|
+
scatter = plt.scatter(
|
|
109
|
+
kb_vectors_2d[:, 0],
|
|
110
|
+
kb_vectors_2d[:, 1],
|
|
111
|
+
alpha=0.5,
|
|
112
|
+
s=40,
|
|
113
|
+
c=range(len(kb_vectors_2d)),
|
|
114
|
+
cmap="viridis",
|
|
115
|
+
label="Knowledge Base Vectors",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# 绘制查询向量(红色 X)
|
|
119
|
+
plt.scatter(
|
|
120
|
+
query_vector_2d[0],
|
|
121
|
+
query_vector_2d[1],
|
|
122
|
+
c="red",
|
|
123
|
+
s=300,
|
|
124
|
+
marker="X",
|
|
125
|
+
edgecolors="black",
|
|
126
|
+
linewidths=2,
|
|
127
|
+
label="Query",
|
|
128
|
+
zorder=5,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# 添加查询文本标注
|
|
132
|
+
plt.annotate(
|
|
133
|
+
"Query",
|
|
134
|
+
(query_vector_2d[0], query_vector_2d[1]),
|
|
135
|
+
xytext=(10, 10),
|
|
136
|
+
textcoords="offset points",
|
|
137
|
+
fontsize=10,
|
|
138
|
+
bbox={"boxstyle": "round,pad=0.5", "fc": "yellow", "alpha": 0.7},
|
|
139
|
+
arrowprops={"arrowstyle": "->", "connectionstyle": "arc3,rad=0"},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
plt.colorbar(scatter, label="Vector Index")
|
|
143
|
+
plt.title(
|
|
144
|
+
f"t-SNE Visualization: Query in Knowledge Base\n"
|
|
145
|
+
f"({index.ntotal} vectors, {index.d} dimensions, KB: {kb.kb_name})",
|
|
146
|
+
fontsize=14,
|
|
147
|
+
pad=20,
|
|
148
|
+
)
|
|
149
|
+
plt.xlabel("t-SNE Dimension 1", fontsize=12)
|
|
150
|
+
plt.ylabel("t-SNE Dimension 2", fontsize=12)
|
|
151
|
+
plt.grid(True, alpha=0.3)
|
|
152
|
+
plt.legend(fontsize=10, loc="upper right")
|
|
153
|
+
|
|
154
|
+
# base64 编码图片返回
|
|
155
|
+
buffer = BytesIO()
|
|
156
|
+
plt.savefig(buffer, format="png", dpi=150, bbox_inches="tight")
|
|
157
|
+
plt.close()
|
|
158
|
+
buffer.seek(0)
|
|
159
|
+
img_base64 = base64.b64encode(buffer.read()).decode("utf-8")
|
|
160
|
+
return img_base64
|
|
161
|
+
|
|
162
|
+
except Exception as e:
|
|
163
|
+
logger.error(f"生成 t-SNE 可视化时出错: {e}")
|
|
164
|
+
logger.error(traceback.format_exc())
|
|
165
|
+
return None
|