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,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
配置元数据国际化工具
|
|
3
|
+
|
|
4
|
+
提供配置元数据的国际化键转换功能
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConfigMetadataI18n:
|
|
11
|
+
"""配置元数据国际化转换器"""
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def _get_i18n_key(group: str, section: str, field: str, attr: str) -> str:
|
|
15
|
+
"""
|
|
16
|
+
生成国际化键
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
group: 配置组,如 'ai_group', 'platform_group'
|
|
20
|
+
section: 配置节,如 'agent_runner', 'general'
|
|
21
|
+
field: 字段名,如 'enable', 'default_provider'
|
|
22
|
+
attr: 属性类型,如 'description', 'hint', 'labels'
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
国际化键,格式如: 'ai_group.agent_runner.enable.description'
|
|
26
|
+
"""
|
|
27
|
+
if field:
|
|
28
|
+
return f"{group}.{section}.{field}.{attr}"
|
|
29
|
+
else:
|
|
30
|
+
return f"{group}.{section}.{attr}"
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def convert_to_i18n_keys(metadata: dict[str, Any]) -> dict[str, Any]:
|
|
34
|
+
"""
|
|
35
|
+
将配置元数据转换为使用国际化键
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
metadata: 原始配置元数据字典
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
使用国际化键的配置元数据字典
|
|
42
|
+
"""
|
|
43
|
+
result = {}
|
|
44
|
+
|
|
45
|
+
for group_key, group_data in metadata.items():
|
|
46
|
+
group_result = {
|
|
47
|
+
"name": f"{group_key}.name",
|
|
48
|
+
"metadata": {},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for section_key, section_data in group_data.get("metadata", {}).items():
|
|
52
|
+
section_result = {
|
|
53
|
+
"description": f"{group_key}.{section_key}.description",
|
|
54
|
+
"type": section_data.get("type"),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# 复制其他属性
|
|
58
|
+
for key in ["items", "condition", "_special", "invisible"]:
|
|
59
|
+
if key in section_data:
|
|
60
|
+
section_result[key] = section_data[key]
|
|
61
|
+
|
|
62
|
+
# 处理 hint
|
|
63
|
+
if "hint" in section_data:
|
|
64
|
+
section_result["hint"] = f"{group_key}.{section_key}.hint"
|
|
65
|
+
|
|
66
|
+
# 处理 items 中的字段
|
|
67
|
+
if "items" in section_data and isinstance(section_data["items"], dict):
|
|
68
|
+
items_result = {}
|
|
69
|
+
for field_key, field_data in section_data["items"].items():
|
|
70
|
+
# 处理嵌套的点号字段名(如 provider_settings.enable)
|
|
71
|
+
field_name = field_key
|
|
72
|
+
|
|
73
|
+
field_result = {}
|
|
74
|
+
|
|
75
|
+
# 复制基本属性
|
|
76
|
+
for attr in [
|
|
77
|
+
"type",
|
|
78
|
+
"condition",
|
|
79
|
+
"_special",
|
|
80
|
+
"invisible",
|
|
81
|
+
"options",
|
|
82
|
+
]:
|
|
83
|
+
if attr in field_data:
|
|
84
|
+
field_result[attr] = field_data[attr]
|
|
85
|
+
|
|
86
|
+
# 转换文本属性为国际化键
|
|
87
|
+
if "description" in field_data:
|
|
88
|
+
field_result["description"] = (
|
|
89
|
+
f"{group_key}.{section_key}.{field_name}.description"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if "hint" in field_data:
|
|
93
|
+
field_result["hint"] = (
|
|
94
|
+
f"{group_key}.{section_key}.{field_name}.hint"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if "labels" in field_data:
|
|
98
|
+
field_result["labels"] = (
|
|
99
|
+
f"{group_key}.{section_key}.{field_name}.labels"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
items_result[field_key] = field_result
|
|
103
|
+
|
|
104
|
+
section_result["items"] = items_result
|
|
105
|
+
|
|
106
|
+
group_result["metadata"][section_key] = section_result
|
|
107
|
+
|
|
108
|
+
result[group_key] = group_result
|
|
109
|
+
|
|
110
|
+
return result
|
astrbot/core/conversation_mgr.py
CHANGED
|
@@ -1,56 +1,109 @@
|
|
|
1
|
-
"""
|
|
2
|
-
AstrBot 会话-对话管理器, 维护两个本地存储, 其中一个是 json 格式的shared_preferences, 另外一个是数据库
|
|
1
|
+
"""AstrBot 会话-对话管理器, 维护两个本地存储, 其中一个是 json 格式的shared_preferences, 另外一个是数据库.
|
|
3
2
|
|
|
4
3
|
在 AstrBot 中, 会话和对话是独立的, 会话用于标记对话窗口, 例如群聊"123456789"可以建立一个会话,
|
|
5
4
|
在一个会话中可以建立多个对话, 并且支持对话的切换和删除
|
|
6
5
|
"""
|
|
7
6
|
|
|
8
|
-
import uuid
|
|
9
7
|
import json
|
|
10
|
-
import
|
|
8
|
+
from collections.abc import Awaitable, Callable
|
|
9
|
+
|
|
11
10
|
from astrbot.core import sp
|
|
12
|
-
from
|
|
11
|
+
from astrbot.core.agent.message import AssistantMessageSegment, UserMessageSegment
|
|
13
12
|
from astrbot.core.db import BaseDatabase
|
|
14
|
-
from astrbot.core.db.po import Conversation
|
|
13
|
+
from astrbot.core.db.po import Conversation, ConversationV2
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
class ConversationManager:
|
|
18
17
|
"""负责管理会话与 LLM 的对话,某个会话当前正在用哪个对话。"""
|
|
19
18
|
|
|
20
19
|
def __init__(self, db_helper: BaseDatabase):
|
|
21
|
-
|
|
22
|
-
self.session_conversations: Dict[str, str] = sp.get("session_conversation", {})
|
|
20
|
+
self.session_conversations: dict[str, str] = {}
|
|
23
21
|
self.db = db_helper
|
|
24
22
|
self.save_interval = 60 # 每 60 秒保存一次
|
|
25
|
-
self._start_periodic_save()
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
# 会话删除回调函数列表(用于级联清理,如知识库配置)
|
|
25
|
+
self._on_session_deleted_callbacks: list[Callable[[str], Awaitable[None]]] = []
|
|
26
|
+
|
|
27
|
+
def register_on_session_deleted(
|
|
28
|
+
self,
|
|
29
|
+
callback: Callable[[str], Awaitable[None]],
|
|
30
|
+
) -> None:
|
|
31
|
+
"""注册会话删除回调函数.
|
|
32
|
+
|
|
33
|
+
其他模块可以注册回调来响应会话删除事件,实现级联清理。
|
|
34
|
+
例如:知识库模块可以注册回调来清理会话的知识库配置。
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
callback: 回调函数,接收会话ID (unified_msg_origin) 作为参数
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
self._on_session_deleted_callbacks.append(callback)
|
|
30
41
|
|
|
31
|
-
async def
|
|
32
|
-
"""
|
|
33
|
-
while True:
|
|
34
|
-
await asyncio.sleep(self.save_interval)
|
|
35
|
-
self._save_to_storage()
|
|
42
|
+
async def _trigger_session_deleted(self, unified_msg_origin: str) -> None:
|
|
43
|
+
"""触发会话删除回调.
|
|
36
44
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
sp.put("session_conversation", self.session_conversations)
|
|
45
|
+
Args:
|
|
46
|
+
unified_msg_origin: 会话ID
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
"""
|
|
49
|
+
for callback in self._on_session_deleted_callbacks:
|
|
50
|
+
try:
|
|
51
|
+
await callback(unified_msg_origin)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
from astrbot.core import logger
|
|
54
|
+
|
|
55
|
+
logger.error(
|
|
56
|
+
f"会话删除回调执行失败 (session: {unified_msg_origin}): {e}",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def _convert_conv_from_v2_to_v1(self, conv_v2: ConversationV2) -> Conversation:
|
|
60
|
+
"""将 ConversationV2 对象转换为 Conversation 对象"""
|
|
61
|
+
created_at = int(conv_v2.created_at.timestamp())
|
|
62
|
+
updated_at = int(conv_v2.updated_at.timestamp())
|
|
63
|
+
return Conversation(
|
|
64
|
+
platform_id=conv_v2.platform_id,
|
|
65
|
+
user_id=conv_v2.user_id,
|
|
66
|
+
cid=conv_v2.conversation_id,
|
|
67
|
+
history=json.dumps(conv_v2.content or []),
|
|
68
|
+
title=conv_v2.title,
|
|
69
|
+
persona_id=conv_v2.persona_id,
|
|
70
|
+
created_at=created_at,
|
|
71
|
+
updated_at=updated_at,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
async def new_conversation(
|
|
75
|
+
self,
|
|
76
|
+
unified_msg_origin: str,
|
|
77
|
+
platform_id: str | None = None,
|
|
78
|
+
content: list[dict] | None = None,
|
|
79
|
+
title: str | None = None,
|
|
80
|
+
persona_id: str | None = None,
|
|
81
|
+
) -> str:
|
|
82
|
+
"""新建对话,并将当前会话的对话转移到新对话.
|
|
43
83
|
|
|
44
84
|
Args:
|
|
45
85
|
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
|
46
86
|
Returns:
|
|
47
87
|
conversation_id (str): 对话 ID, 是 uuid 格式的字符串
|
|
88
|
+
|
|
48
89
|
"""
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
90
|
+
if not platform_id:
|
|
91
|
+
# 如果没有提供 platform_id,则从 unified_msg_origin 中解析
|
|
92
|
+
parts = unified_msg_origin.split(":")
|
|
93
|
+
if len(parts) >= 3:
|
|
94
|
+
platform_id = parts[0]
|
|
95
|
+
if not platform_id:
|
|
96
|
+
platform_id = "unknown"
|
|
97
|
+
conv = await self.db.create_conversation(
|
|
98
|
+
user_id=unified_msg_origin,
|
|
99
|
+
platform_id=platform_id,
|
|
100
|
+
content=content,
|
|
101
|
+
title=title,
|
|
102
|
+
persona_id=persona_id,
|
|
103
|
+
)
|
|
104
|
+
self.session_conversations[unified_msg_origin] = conv.conversation_id
|
|
105
|
+
await sp.session_put(unified_msg_origin, "sel_conv_id", conv.conversation_id)
|
|
106
|
+
return conv.conversation_id
|
|
54
107
|
|
|
55
108
|
async def switch_conversation(self, unified_msg_origin: str, conversation_id: str):
|
|
56
109
|
"""切换会话的对话
|
|
@@ -58,137 +111,294 @@ class ConversationManager:
|
|
|
58
111
|
Args:
|
|
59
112
|
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
|
60
113
|
conversation_id (str): 对话 ID, 是 uuid 格式的字符串
|
|
114
|
+
|
|
61
115
|
"""
|
|
62
116
|
self.session_conversations[unified_msg_origin] = conversation_id
|
|
63
|
-
sp.
|
|
117
|
+
await sp.session_put(unified_msg_origin, "sel_conv_id", conversation_id)
|
|
64
118
|
|
|
65
119
|
async def delete_conversation(
|
|
66
|
-
self,
|
|
120
|
+
self,
|
|
121
|
+
unified_msg_origin: str,
|
|
122
|
+
conversation_id: str | None = None,
|
|
67
123
|
):
|
|
68
124
|
"""删除会话的对话,当 conversation_id 为 None 时删除会话当前的对话
|
|
69
125
|
|
|
70
126
|
Args:
|
|
71
127
|
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
|
72
128
|
conversation_id (str): 对话 ID, 是 uuid 格式的字符串
|
|
129
|
+
|
|
73
130
|
"""
|
|
74
|
-
|
|
131
|
+
if not conversation_id:
|
|
132
|
+
conversation_id = self.session_conversations.get(unified_msg_origin)
|
|
75
133
|
if conversation_id:
|
|
76
|
-
self.db.delete_conversation(
|
|
77
|
-
|
|
78
|
-
|
|
134
|
+
await self.db.delete_conversation(cid=conversation_id)
|
|
135
|
+
curr_cid = await self.get_curr_conversation_id(unified_msg_origin)
|
|
136
|
+
if curr_cid == conversation_id:
|
|
137
|
+
self.session_conversations.pop(unified_msg_origin, None)
|
|
138
|
+
await sp.session_remove(unified_msg_origin, "sel_conv_id")
|
|
139
|
+
|
|
140
|
+
async def delete_conversations_by_user_id(self, unified_msg_origin: str):
|
|
141
|
+
"""删除会话的所有对话
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
|
79
145
|
|
|
80
|
-
|
|
146
|
+
"""
|
|
147
|
+
await self.db.delete_conversations_by_user_id(user_id=unified_msg_origin)
|
|
148
|
+
self.session_conversations.pop(unified_msg_origin, None)
|
|
149
|
+
await sp.session_remove(unified_msg_origin, "sel_conv_id")
|
|
150
|
+
|
|
151
|
+
# 触发会话删除回调(级联清理)
|
|
152
|
+
await self._trigger_session_deleted(unified_msg_origin)
|
|
153
|
+
|
|
154
|
+
async def get_curr_conversation_id(self, unified_msg_origin: str) -> str | None:
|
|
81
155
|
"""获取会话当前的对话 ID
|
|
82
156
|
|
|
83
157
|
Args:
|
|
84
158
|
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
|
85
159
|
Returns:
|
|
86
160
|
conversation_id (str): 对话 ID, 是 uuid 格式的字符串
|
|
161
|
+
|
|
87
162
|
"""
|
|
88
|
-
|
|
163
|
+
ret = self.session_conversations.get(unified_msg_origin, None)
|
|
164
|
+
if not ret:
|
|
165
|
+
ret = await sp.session_get(unified_msg_origin, "sel_conv_id", None)
|
|
166
|
+
if ret:
|
|
167
|
+
self.session_conversations[unified_msg_origin] = ret
|
|
168
|
+
return ret
|
|
89
169
|
|
|
90
170
|
async def get_conversation(
|
|
91
|
-
self,
|
|
92
|
-
|
|
93
|
-
|
|
171
|
+
self,
|
|
172
|
+
unified_msg_origin: str,
|
|
173
|
+
conversation_id: str,
|
|
174
|
+
create_if_not_exists: bool = False,
|
|
175
|
+
) -> Conversation | None:
|
|
176
|
+
"""获取会话的对话.
|
|
94
177
|
|
|
95
178
|
Args:
|
|
96
179
|
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
|
97
180
|
conversation_id (str): 对话 ID, 是 uuid 格式的字符串
|
|
181
|
+
create_if_not_exists (bool): 如果对话不存在,是否创建一个新的对话
|
|
98
182
|
Returns:
|
|
99
183
|
conversation (Conversation): 对话对象
|
|
100
|
-
"""
|
|
101
|
-
return self.db.get_conversation_by_user_id(unified_msg_origin, conversation_id)
|
|
102
184
|
|
|
103
|
-
|
|
104
|
-
|
|
185
|
+
"""
|
|
186
|
+
conv = await self.db.get_conversation_by_id(cid=conversation_id)
|
|
187
|
+
if not conv and create_if_not_exists:
|
|
188
|
+
# 如果对话不存在且需要创建,则新建一个对话
|
|
189
|
+
conversation_id = await self.new_conversation(unified_msg_origin)
|
|
190
|
+
conv = await self.db.get_conversation_by_id(cid=conversation_id)
|
|
191
|
+
conv_res = None
|
|
192
|
+
if conv:
|
|
193
|
+
conv_res = self._convert_conv_from_v2_to_v1(conv)
|
|
194
|
+
return conv_res
|
|
195
|
+
|
|
196
|
+
async def get_conversations(
|
|
197
|
+
self,
|
|
198
|
+
unified_msg_origin: str | None = None,
|
|
199
|
+
platform_id: str | None = None,
|
|
200
|
+
) -> list[Conversation]:
|
|
201
|
+
"""获取对话列表.
|
|
105
202
|
|
|
106
203
|
Args:
|
|
107
|
-
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
|
204
|
+
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id,可选
|
|
205
|
+
platform_id (str): 平台 ID, 可选参数, 用于过滤对话
|
|
108
206
|
Returns:
|
|
109
207
|
conversations (List[Conversation]): 对话对象列表
|
|
208
|
+
|
|
209
|
+
"""
|
|
210
|
+
convs = await self.db.get_conversations(
|
|
211
|
+
user_id=unified_msg_origin,
|
|
212
|
+
platform_id=platform_id,
|
|
213
|
+
)
|
|
214
|
+
convs_res = []
|
|
215
|
+
for conv in convs:
|
|
216
|
+
conv_res = self._convert_conv_from_v2_to_v1(conv)
|
|
217
|
+
convs_res.append(conv_res)
|
|
218
|
+
return convs_res
|
|
219
|
+
|
|
220
|
+
async def get_filtered_conversations(
|
|
221
|
+
self,
|
|
222
|
+
page: int = 1,
|
|
223
|
+
page_size: int = 20,
|
|
224
|
+
platform_ids: list[str] | None = None,
|
|
225
|
+
search_query: str = "",
|
|
226
|
+
**kwargs,
|
|
227
|
+
) -> tuple[list[Conversation], int]:
|
|
228
|
+
"""获取过滤后的对话列表.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
page (int): 页码, 默认为 1
|
|
232
|
+
page_size (int): 每页大小, 默认为 20
|
|
233
|
+
platform_ids (list[str]): 平台 ID 列表, 可选
|
|
234
|
+
search_query (str): 搜索查询字符串, 可选
|
|
235
|
+
Returns:
|
|
236
|
+
conversations (list[Conversation]): 对话对象列表
|
|
237
|
+
|
|
110
238
|
"""
|
|
111
|
-
|
|
239
|
+
convs, cnt = await self.db.get_filtered_conversations(
|
|
240
|
+
page=page,
|
|
241
|
+
page_size=page_size,
|
|
242
|
+
platform_ids=platform_ids,
|
|
243
|
+
search_query=search_query,
|
|
244
|
+
**kwargs,
|
|
245
|
+
)
|
|
246
|
+
convs_res = []
|
|
247
|
+
for conv in convs:
|
|
248
|
+
conv_res = self._convert_conv_from_v2_to_v1(conv)
|
|
249
|
+
convs_res.append(conv_res)
|
|
250
|
+
return convs_res, cnt
|
|
112
251
|
|
|
113
252
|
async def update_conversation(
|
|
114
|
-
self,
|
|
115
|
-
|
|
116
|
-
|
|
253
|
+
self,
|
|
254
|
+
unified_msg_origin: str,
|
|
255
|
+
conversation_id: str | None = None,
|
|
256
|
+
history: list[dict] | None = None,
|
|
257
|
+
title: str | None = None,
|
|
258
|
+
persona_id: str | None = None,
|
|
259
|
+
) -> None:
|
|
260
|
+
"""更新会话的对话.
|
|
117
261
|
|
|
118
262
|
Args:
|
|
119
263
|
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
|
120
264
|
conversation_id (str): 对话 ID, 是 uuid 格式的字符串
|
|
121
265
|
history (List[Dict]): 对话历史记录, 是一个字典列表, 每个字典包含 role 和 content 字段
|
|
266
|
+
|
|
122
267
|
"""
|
|
268
|
+
if not conversation_id:
|
|
269
|
+
# 如果没有提供 conversation_id,则获取当前的
|
|
270
|
+
conversation_id = await self.get_curr_conversation_id(unified_msg_origin)
|
|
123
271
|
if conversation_id:
|
|
124
|
-
self.db.update_conversation(
|
|
125
|
-
user_id=unified_msg_origin,
|
|
272
|
+
await self.db.update_conversation(
|
|
126
273
|
cid=conversation_id,
|
|
127
|
-
|
|
274
|
+
title=title,
|
|
275
|
+
persona_id=persona_id,
|
|
276
|
+
content=history,
|
|
128
277
|
)
|
|
129
278
|
|
|
130
|
-
async def update_conversation_title(
|
|
131
|
-
|
|
279
|
+
async def update_conversation_title(
|
|
280
|
+
self,
|
|
281
|
+
unified_msg_origin: str,
|
|
282
|
+
title: str,
|
|
283
|
+
conversation_id: str | None = None,
|
|
284
|
+
) -> None:
|
|
285
|
+
"""更新会话的对话标题.
|
|
132
286
|
|
|
133
287
|
Args:
|
|
134
288
|
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
|
135
289
|
title (str): 对话标题
|
|
290
|
+
conversation_id (str): 对话 ID, 是 uuid 格式的字符串
|
|
291
|
+
Deprecated:
|
|
292
|
+
Use `update_conversation` with `title` parameter instead.
|
|
293
|
+
|
|
136
294
|
"""
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
295
|
+
await self.update_conversation(
|
|
296
|
+
unified_msg_origin=unified_msg_origin,
|
|
297
|
+
conversation_id=conversation_id,
|
|
298
|
+
title=title,
|
|
299
|
+
)
|
|
142
300
|
|
|
143
301
|
async def update_conversation_persona_id(
|
|
144
|
-
self,
|
|
145
|
-
|
|
146
|
-
|
|
302
|
+
self,
|
|
303
|
+
unified_msg_origin: str,
|
|
304
|
+
persona_id: str,
|
|
305
|
+
conversation_id: str | None = None,
|
|
306
|
+
) -> None:
|
|
307
|
+
"""更新会话的对话 Persona ID.
|
|
147
308
|
|
|
148
309
|
Args:
|
|
149
310
|
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
|
150
311
|
persona_id (str): 对话 Persona ID
|
|
312
|
+
conversation_id (str): 对话 ID, 是 uuid 格式的字符串
|
|
313
|
+
Deprecated:
|
|
314
|
+
Use `update_conversation` with `persona_id` parameter instead.
|
|
315
|
+
|
|
151
316
|
"""
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
317
|
+
await self.update_conversation(
|
|
318
|
+
unified_msg_origin=unified_msg_origin,
|
|
319
|
+
conversation_id=conversation_id,
|
|
320
|
+
persona_id=persona_id,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
async def add_message_pair(
|
|
324
|
+
self,
|
|
325
|
+
cid: str,
|
|
326
|
+
user_message: UserMessageSegment | dict,
|
|
327
|
+
assistant_message: AssistantMessageSegment | dict,
|
|
328
|
+
) -> None:
|
|
329
|
+
"""Add a user-assistant message pair to the conversation history.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
cid (str): Conversation ID
|
|
333
|
+
user_message (UserMessageSegment | dict): OpenAI-format user message object or dict
|
|
334
|
+
assistant_message (AssistantMessageSegment | dict): OpenAI-format assistant message object or dict
|
|
335
|
+
|
|
336
|
+
Raises:
|
|
337
|
+
Exception: If the conversation with the given ID is not found
|
|
338
|
+
"""
|
|
339
|
+
conv = await self.db.get_conversation_by_id(cid=cid)
|
|
340
|
+
if not conv:
|
|
341
|
+
raise Exception(f"Conversation with id {cid} not found")
|
|
342
|
+
history = conv.content or []
|
|
343
|
+
if isinstance(user_message, UserMessageSegment):
|
|
344
|
+
user_msg_dict = user_message.model_dump()
|
|
345
|
+
else:
|
|
346
|
+
user_msg_dict = user_message
|
|
347
|
+
if isinstance(assistant_message, AssistantMessageSegment):
|
|
348
|
+
assistant_msg_dict = assistant_message.model_dump()
|
|
349
|
+
else:
|
|
350
|
+
assistant_msg_dict = assistant_message
|
|
351
|
+
history.append(user_msg_dict)
|
|
352
|
+
history.append(assistant_msg_dict)
|
|
353
|
+
await self.db.update_conversation(
|
|
354
|
+
cid=cid,
|
|
355
|
+
content=history,
|
|
356
|
+
)
|
|
157
357
|
|
|
158
358
|
async def get_human_readable_context(
|
|
159
|
-
self,
|
|
160
|
-
|
|
161
|
-
|
|
359
|
+
self,
|
|
360
|
+
unified_msg_origin: str,
|
|
361
|
+
conversation_id: str,
|
|
362
|
+
page: int = 1,
|
|
363
|
+
page_size: int = 10,
|
|
364
|
+
) -> tuple[list[str], int]:
|
|
365
|
+
"""获取人类可读的上下文.
|
|
162
366
|
|
|
163
367
|
Args:
|
|
164
368
|
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
|
165
369
|
conversation_id (str): 对话 ID, 是 uuid 格式的字符串
|
|
166
370
|
page (int): 页码
|
|
167
371
|
page_size (int): 每页大小
|
|
372
|
+
|
|
168
373
|
"""
|
|
169
374
|
conversation = await self.get_conversation(unified_msg_origin, conversation_id)
|
|
375
|
+
if not conversation:
|
|
376
|
+
return [], 0
|
|
170
377
|
history = json.loads(conversation.history)
|
|
171
378
|
|
|
172
|
-
|
|
173
|
-
|
|
379
|
+
# contexts_groups 存放按顺序的段落(每个段落是一个 str 列表),
|
|
380
|
+
# 之后会被展平成一个扁平的 str 列表返回。
|
|
381
|
+
contexts_groups: list[list[str]] = []
|
|
382
|
+
temp_contexts: list[str] = []
|
|
174
383
|
for record in history:
|
|
175
384
|
if record["role"] == "user":
|
|
176
385
|
temp_contexts.append(f"User: {record['content']}")
|
|
177
386
|
elif record["role"] == "assistant":
|
|
178
|
-
if
|
|
387
|
+
if record.get("content"):
|
|
179
388
|
temp_contexts.append(f"Assistant: {record['content']}")
|
|
180
389
|
elif "tool_calls" in record:
|
|
181
390
|
tool_calls_str = json.dumps(
|
|
182
|
-
record["tool_calls"],
|
|
391
|
+
record["tool_calls"],
|
|
392
|
+
ensure_ascii=False,
|
|
183
393
|
)
|
|
184
394
|
temp_contexts.append(f"Assistant: [函数调用] {tool_calls_str}")
|
|
185
395
|
else:
|
|
186
396
|
temp_contexts.append("Assistant: [未知的内容]")
|
|
187
|
-
|
|
397
|
+
contexts_groups.insert(0, temp_contexts)
|
|
188
398
|
temp_contexts = []
|
|
189
399
|
|
|
190
|
-
#
|
|
191
|
-
contexts = [item for sublist in
|
|
400
|
+
# 展平分组后的 contexts 列表为单层字符串列表
|
|
401
|
+
contexts = [item for sublist in contexts_groups for item in sublist]
|
|
192
402
|
|
|
193
403
|
# 计算分页
|
|
194
404
|
paged_contexts = contexts[(page - 1) * page_size : page * page_size]
|