├── .devcontainer
└── devcontainer.json
├── .dockerignore
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.yml
│ └── feature_request.yml
├── dependabot.yml
├── prompts
│ └── chat.prompt.md
└── workflows
│ └── docker-image.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── PRIVACY.md
├── README.md
├── TODO.md
├── __main__.py
├── bot.py
├── changelogs
├── changelog.md
└── changes.md
├── depends-data
├── char_frequency.json
└── dict.txt
├── docker-compose.yml
├── docs
├── MCP_SIMPLE_GUIDE.md
├── MCP_SSE_INTEGRATION.md
├── MCP_SSE_QUICKSTART.md
├── MCP_SSE_USAGE.md
├── MCP_TOOLS_INTEGRATION.md
├── affinity_flow_guide.md
├── architecture
│ ├── PERMISSION_SYSTEM.md
│ └── memory_system_design_v3.md
├── assets
│ ├── 1750326700269.png
│ ├── 1750332508760.png
│ ├── image-1.png
│ └── image.png
├── deployment_guide.md
├── development
│ └── CONTRIBUTE.md
├── guides
│ ├── model_configuration_guide.md
│ └── vector_db_usage_guide.md
├── integrations
│ └── Bing.md
└── plugins
│ ├── PLUS_COMMAND_GUIDE.md
│ ├── action-components.md
│ ├── api
│ ├── adapter-command-api.md
│ ├── chat-api.md
│ ├── component-manage-api.md
│ ├── config-api.md
│ ├── database-api.md
│ ├── emoji-api.md
│ ├── generator-api.md
│ ├── llm-api.md
│ ├── logging-api.md
│ ├── message-api.md
│ ├── person-api.md
│ ├── plugin-manage-api.md
│ ├── send-api.md
│ └── tool-api.md
│ ├── configuration-guide.md
│ ├── dependency-management.md
│ ├── event-system-guide.md
│ ├── index.md
│ ├── manifest-guide.md
│ ├── quick-start.md
│ └── tool_guide.md
├── plugins
├── bilibli
│ ├── __init__.py
│ ├── _manifest.json
│ ├── bilibli_base.py
│ └── plugin.py
├── hello_world_plugin
│ ├── __init__.py
│ ├── _manifest.json
│ └── plugin.py
└── set_emoji_like
│ ├── __init__.py
│ └── _manifest.json
├── pyproject.toml
├── pyrightconfig.json
├── requirements.lock
├── requirements.txt
├── scripts
├── convert_sqlalchemy_models.py
├── expression_stats.py
├── lpmm_learning_tool.py
├── rebuild_metadata_index.py
├── run_lpmm.sh
├── run_multi_stage_smoke.py
├── text_length_analysis.py
└── update_prompt_imports.py
├── src
├── __init__.py
├── api
│ ├── __init__.py
│ └── message_router.py
├── chat
│ ├── __init__.py
│ ├── antipromptinjector
│ │ ├── __init__.py
│ │ ├── anti_injector.py
│ │ ├── core
│ │ │ ├── __init__.py
│ │ │ ├── detector.py
│ │ │ └── shield.py
│ │ ├── counter_attack.py
│ │ ├── decision
│ │ │ ├── __init__.py
│ │ │ ├── counter_attack.py
│ │ │ └── decision_maker.py
│ │ ├── decision_maker.py
│ │ ├── detector.py
│ │ ├── management
│ │ │ ├── __init__.py
│ │ │ ├── statistics.py
│ │ │ └── user_ban.py
│ │ ├── processors
│ │ │ ├── __init__.py
│ │ │ └── message_processor.py
│ │ └── types.py
│ ├── chatter_manager.py
│ ├── emoji_system
│ │ ├── emoji_history.py
│ │ └── emoji_manager.py
│ ├── energy_system
│ │ ├── __init__.py
│ │ └── energy_manager.py
│ ├── express
│ │ ├── expression_learner.py
│ │ └── expression_selector.py
│ ├── frequency_analyzer
│ │ ├── analyzer.py
│ │ ├── tracker.py
│ │ └── trigger.py
│ ├── interest_system
│ │ ├── __init__.py
│ │ ├── bot_interest_manager.py
│ │ └── interest_manager.py
│ ├── knowledge
│ │ ├── LICENSE
│ │ ├── __init__.py
│ │ ├── embedding_store.py
│ │ ├── global_logger.py
│ │ ├── ie_process.py
│ │ ├── kg_manager.py
│ │ ├── knowledge_lib.py
│ │ ├── open_ie.py
│ │ ├── prompt_template.py
│ │ ├── qa_manager.py
│ │ └── utils
│ │ │ ├── __init__.py
│ │ │ ├── dyn_topk.py
│ │ │ ├── hash.py
│ │ │ └── json_fix.py
│ ├── memory_system
│ │ ├── __init__.py
│ │ ├── enhanced_memory_activator.py
│ │ ├── hippocampus_sampler.py
│ │ ├── memory_activator_new.py
│ │ ├── memory_builder.py
│ │ ├── memory_chunk.py
│ │ ├── memory_forgetting_engine.py
│ │ ├── memory_formatter.py
│ │ ├── memory_fusion.py
│ │ ├── memory_manager.py
│ │ ├── memory_metadata_index.py
│ │ ├── memory_query_planner.py
│ │ ├── memory_system.py
│ │ └── vector_memory_storage_v2.py
│ ├── message_manager
│ │ ├── __init__.py
│ │ ├── adaptive_stream_manager.py
│ │ ├── batch_database_writer.py
│ │ ├── context_manager.py
│ │ ├── distribution_manager.py
│ │ ├── message_manager.py
│ │ ├── sleep_manager
│ │ │ ├── notification_sender.py
│ │ │ ├── sleep_manager.py
│ │ │ ├── sleep_state.py
│ │ │ ├── time_checker.py
│ │ │ ├── wakeup_context.py
│ │ │ └── wakeup_manager.py
│ │ └── stream_cache_manager.py
│ ├── message_receive
│ │ ├── __init__.py
│ │ ├── bot.py
│ │ ├── chat_stream.py
│ │ ├── message.py
│ │ ├── optimized_chat_stream.py
│ │ ├── storage.py
│ │ └── uni_message_sender.py
│ ├── planner_actions
│ │ ├── action_manager.py
│ │ └── action_modifier.py
│ ├── replyer
│ │ ├── default_generator.py
│ │ └── replyer_manager.py
│ └── utils
│ │ ├── chat_message_builder.py
│ │ ├── memory_mappings.py
│ │ ├── prompt.py
│ │ ├── statistic.py
│ │ ├── timer_calculator.py
│ │ ├── typo_generator.py
│ │ ├── utils.py
│ │ ├── utils_image.py
│ │ ├── utils_video.py
│ │ ├── utils_video_legacy.py
│ │ └── utils_voice.py
├── common
│ ├── __init__.py
│ ├── cache_manager.py
│ ├── config_helpers.py
│ ├── data_models
│ │ ├── __init__.py
│ │ ├── bot_interest_data_model.py
│ │ ├── database_data_model.py
│ │ ├── info_data_model.py
│ │ ├── llm_data_model.py
│ │ └── message_manager_data_model.py
│ ├── database
│ │ ├── __init__.py
│ │ ├── connection_pool_manager.py
│ │ ├── database.py
│ │ ├── db_batch_scheduler.py
│ │ ├── db_migration.py
│ │ ├── sqlalchemy_database_api.py
│ │ ├── sqlalchemy_init.py
│ │ ├── sqlalchemy_models.py
│ │ └── sqlalchemy_models.py.bak
│ ├── logger.py
│ ├── message
│ │ ├── __init__.py
│ │ └── api.py
│ ├── message_repository.py
│ ├── remote.py
│ ├── server.py
│ ├── tcp_connector.py
│ └── vector_db
│ │ ├── __init__.py
│ │ ├── base.py
│ │ └── chromadb_impl.py
├── config
│ ├── api_ada_configs.py
│ ├── config.py
│ ├── config_base.py
│ └── official_configs.py
├── individuality
│ ├── individuality.py
│ └── not_using
│ │ ├── offline_llm.py
│ │ ├── per_bf_gen.py
│ │ ├── questionnaire.py
│ │ ├── scene.py
│ │ └── template_scene.json
├── llm_models
│ ├── LICENSE
│ ├── __init__.py
│ ├── exceptions.py
│ ├── model_client
│ │ ├── __init__.py
│ │ ├── aiohttp_gemini_client.py
│ │ ├── base_client.py
│ │ ├── mcp_sse_client.py
│ │ └── openai_client.py
│ ├── payload_content
│ │ ├── __init__.py
│ │ ├── message.py
│ │ ├── resp_format.py
│ │ └── tool_option.py
│ ├── utils.py
│ └── utils_model.py
├── main.py
├── mais4u
│ ├── config
│ │ ├── old
│ │ │ └── s4u_config_20250715_141713.toml
│ │ ├── s4u_config.toml
│ │ └── s4u_config_template.toml
│ ├── constant_s4u.py
│ ├── mai_think.py
│ ├── mais4u_chat
│ │ ├── body_emotion_action_manager.py
│ │ ├── context_web_manager.py
│ │ ├── gift_manager.py
│ │ ├── internal_manager.py
│ │ ├── s4u_chat.py
│ │ ├── s4u_mood_manager.py
│ │ ├── s4u_msg_processor.py
│ │ ├── s4u_prompt.py
│ │ ├── s4u_stream_generator.py
│ │ ├── s4u_watching_manager.py
│ │ ├── screen_manager.py
│ │ ├── super_chat_manager.py
│ │ └── yes_or_no.py
│ ├── openai_client.py
│ └── s4u_config.py
├── manager
│ ├── async_task_manager.py
│ └── local_store_manager.py
├── mood
│ └── mood_manager.py
├── person_info
│ ├── person_info.py
│ ├── relationship_builder.py
│ ├── relationship_builder_manager.py
│ ├── relationship_fetcher.py
│ └── relationship_manager.py
├── plugin_system
│ ├── __init__.py
│ ├── apis
│ │ ├── __init__.py
│ │ ├── chat_api.py
│ │ ├── component_manage_api.py
│ │ ├── config_api.py
│ │ ├── cross_context_api.py
│ │ ├── database_api.py
│ │ ├── emoji_api.py
│ │ ├── generator_api.py
│ │ ├── llm_api.py
│ │ ├── logging_api.py
│ │ ├── message_api.py
│ │ ├── permission_api.py
│ │ ├── person_api.py
│ │ ├── plugin_manage_api.py
│ │ ├── plugin_register_api.py
│ │ ├── schedule_api.py
│ │ ├── scoring_api.py
│ │ ├── send_api.py
│ │ └── tool_api.py
│ ├── base
│ │ ├── __init__.py
│ │ ├── base_action.py
│ │ ├── base_chatter.py
│ │ ├── base_command.py
│ │ ├── base_event.py
│ │ ├── base_events_handler.py
│ │ ├── base_interest_calculator.py
│ │ ├── base_plugin.py
│ │ ├── base_tool.py
│ │ ├── command_args.py
│ │ ├── component_types.py
│ │ ├── config_types.py
│ │ ├── plugin_base.py
│ │ ├── plugin_metadata.py
│ │ └── plus_command.py
│ ├── core
│ │ ├── __init__.py
│ │ ├── component_registry.py
│ │ ├── event_manager.py
│ │ ├── global_announcement_manager.py
│ │ ├── permission_manager.py
│ │ ├── plugin_manager.py
│ │ └── tool_use.py
│ ├── services
│ │ ├── interest_service.py
│ │ └── relationship_service.py
│ └── utils
│ │ ├── __init__.py
│ │ ├── dependency_alias.py
│ │ ├── dependency_config.py
│ │ ├── dependency_manager.py
│ │ ├── mcp_connector.py
│ │ ├── mcp_tool_provider.py
│ │ └── permission_decorators.py
├── plugins
│ ├── __init__.py
│ └── built_in
│ │ ├── affinity_flow_chatter
│ │ ├── README.md
│ │ ├── __init__.py
│ │ ├── affinity_chatter.py
│ │ ├── affinity_interest_calculator.py
│ │ ├── plan_executor.py
│ │ ├── plan_filter.py
│ │ ├── plan_generator.py
│ │ ├── planner.py
│ │ ├── planner_prompts.py
│ │ ├── plugin.py
│ │ └── relationship_tracker.py
│ │ ├── core_actions
│ │ ├── __init__.py
│ │ ├── anti_injector_manager.py
│ │ ├── emoji.py
│ │ └── plugin.py
│ │ ├── knowledge
│ │ └── lpmm_get_knowledge.py
│ │ ├── maizone_refactored
│ │ ├── __init__.py
│ │ ├── actions
│ │ │ ├── __init__.py
│ │ │ ├── read_feed_action.py
│ │ │ └── send_feed_action.py
│ │ ├── commands
│ │ │ ├── __init__.py
│ │ │ └── send_feed_command.py
│ │ ├── plugin.py
│ │ ├── services
│ │ │ ├── __init__.py
│ │ │ ├── content_service.py
│ │ │ ├── cookie_service.py
│ │ │ ├── image_service.py
│ │ │ ├── manager.py
│ │ │ ├── monitor_service.py
│ │ │ ├── qzone_service.py
│ │ │ ├── reply_tracker_service.py
│ │ │ └── scheduler_service.py
│ │ └── utils
│ │ │ ├── __init__.py
│ │ │ └── history_utils.py
│ │ ├── napcat_adapter_plugin
│ │ ├── .gitignore
│ │ ├── CONSTS.py
│ │ ├── __init__.py
│ │ ├── event_handlers.py
│ │ ├── event_types.py
│ │ ├── plugin.py
│ │ ├── pyproject.toml
│ │ ├── src
│ │ │ ├── __init__.py
│ │ │ ├── database.py
│ │ │ ├── message_chunker.py
│ │ │ ├── mmc_com_layer.py
│ │ │ ├── recv_handler
│ │ │ │ ├── __init__.py
│ │ │ │ ├── message_handler.py
│ │ │ │ ├── message_sending.py
│ │ │ │ ├── meta_event_handler.py
│ │ │ │ ├── notice_handler.py
│ │ │ │ └── qq_emoji_list.py
│ │ │ ├── response_pool.py
│ │ │ ├── send_handler.py
│ │ │ ├── utils.py
│ │ │ ├── video_handler.py
│ │ │ └── websocket_manager.py
│ │ └── todo.md
│ │ ├── permission_management
│ │ ├── __init__.py
│ │ └── plugin.py
│ │ ├── plugin_management
│ │ ├── __init__.py
│ │ └── plugin.py
│ │ ├── proactive_thinker
│ │ ├── __init__.py
│ │ ├── plugin.py
│ │ ├── proacive_thinker_event.py
│ │ ├── proactive_chatter_refactor_plan.md
│ │ └── proactive_thinker_executor.py
│ │ ├── social_toolkit_plugin
│ │ ├── __init__.py
│ │ ├── plugin.py
│ │ └── qq_emoji_list.py
│ │ ├── tts_plugin
│ │ ├── __init__.py
│ │ └── plugin.py
│ │ └── web_search_tool
│ │ ├── __init__.py
│ │ ├── engines
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── bing_engine.py
│ │ ├── ddg_engine.py
│ │ ├── exa_engine.py
│ │ ├── searxng_engine.py
│ │ └── tavily_engine.py
│ │ ├── plugin.py
│ │ ├── tools
│ │ ├── __init__.py
│ │ ├── url_parser.py
│ │ └── web_search.py
│ │ └── utils
│ │ ├── __init__.py
│ │ ├── api_key_manager.py
│ │ ├── formatters.py
│ │ └── url_utils.py
├── schedule
│ ├── database.py
│ ├── llm_generator.py
│ ├── monthly_plan_manager.py
│ ├── plan_manager.py
│ ├── schedule_manager.py
│ └── schemas.py
└── utils
│ ├── __init__.py
│ ├── message_chunker.py
│ └── timing_utils.py
├── template
├── bot_config_template.toml
├── model_config_template.toml
└── template.env
├── ui_log_adapter.py
└── uv.lock
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "MaiBot-DevContainer",
3 | "image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye",
4 | "features": {
5 | "ghcr.io/rocker-org/devcontainer-features/apt-packages:1": {
6 | "packages": [
7 | "tmux"
8 | ]
9 | },
10 | "ghcr.io/devcontainers/features/github-cli:1": {}
11 | },
12 | "forwardPorts": [
13 | "8000:8000"
14 | ],
15 | "postCreateCommand": "pip3 install --user -r requirements.txt",
16 | "customizations": {
17 | "jetbrains": {
18 | "backend": "PyCharm"
19 | },
20 | "vscode": {
21 | "extensions": [
22 | "tamasfe.even-better-toml",
23 | "njpwerner.autodocstring",
24 | "ms-python.python",
25 | "KevinRose.vsc-python-indent",
26 | "ms-python.vscode-pylance",
27 | "ms-python.autopep8"
28 | ]
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | __pycache__
3 | *.pyo
4 | *.pyd
5 | .DS_Store
6 | mongodb
7 | napcat
8 | docs/
9 | .github/
10 | # test
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: 提交 Bug
3 | labels: ["BUG"]
4 | body:
5 | - type: checkboxes
6 | attributes:
7 | label: "检查项"
8 | description: "请检查下列项目,并勾选确认。"
9 | options:
10 | - label: "我确认此问题在所有分支的最新版本中依旧存在"
11 | required: true
12 | - label: "我确认在 Issues 列表中并无其他人已经提出过与此问题相同或相似的问题"
13 | required: true
14 | - label: "我使用了 Docker"
15 | - type: dropdown
16 | attributes:
17 | label: "使用的分支"
18 | description: "请选择您正在使用的版本分支"
19 | options:
20 | - main
21 | - dev
22 | validations:
23 | required: true
24 | - type: input
25 | attributes:
26 | label: "具体版本号"
27 | description: "请输入您使用的具体版本号"
28 | placeholder: "例如:0.5.11、0.5.8、0.6.0"
29 | validations:
30 | required: true
31 | - type: textarea
32 | attributes:
33 | label: 遇到的问题
34 | validations:
35 | required: true
36 | - type: textarea
37 | attributes:
38 | label: 报错信息
39 | validations:
40 | required: true
41 | - type: textarea
42 | attributes:
43 | label: 如何重现此问题?
44 | placeholder: "若不知道请略过此问题"
45 | - type: textarea
46 | attributes:
47 | label: 可能造成问题的原因
48 | placeholder: "若不知道请略过此问题"
49 | - type: textarea
50 | attributes:
51 | label: 系统环境
52 | placeholder: "例如:Windows 11 专业版 64位 24H2 / Debian Bookworm"
53 | validations:
54 | required: true
55 | - type: textarea
56 | attributes:
57 | label: Python 版本
58 | placeholder: "例如:Python 3.11"
59 | validations:
60 | required: true
61 | - type: textarea
62 | attributes:
63 | label: 补充信息
64 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: Feature Request
2 | description: 新功能请求
3 | labels: ["Feature"]
4 | body:
5 | - type: checkboxes
6 | attributes:
7 | label: "检查项"
8 | description: "请检查下列项目,并勾选确认。"
9 | options:
10 | - label: "我确认在Issues列表中并无其他人已经建议过相似的功能"
11 | required: true
12 | - label: "这个新功能可以解决目前存在的某个问题或BUG"
13 | - label: "你已经更新了最新的dev分支,但是你的问题依然没有被解决"
14 | - type: textarea
15 | attributes:
16 | label: 期望的功能描述
17 | validations:
18 | required: true
19 | - type: textarea
20 | attributes:
21 | label: 补充信息
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 |
--------------------------------------------------------------------------------
/.github/prompts/chat.prompt.md:
--------------------------------------------------------------------------------
1 | ---
2 | mode: agent
3 | ---
4 | 记得执行前激活虚拟环境,用的shell是powershell与linux语法有区别
--------------------------------------------------------------------------------
/.github/workflows/docker-image.yml:
--------------------------------------------------------------------------------
1 | name: Docker CI
2 |
3 | on:
4 | # push:
5 | # branches:
6 | # - master
7 | # - develop
8 | # tags:
9 | # - "v*.*.*"
10 | # - "v*"
11 | # - "*.*.*"
12 | # - "*.*.*-*"
13 | workflow_dispatch: # 允许手动触发工作流
14 |
15 | jobs:
16 | build-amd64:
17 | name: 构建 AMD64 镜像
18 | runs-on: ubuntu-24.04
19 | outputs:
20 | digest: ${{ steps.build.outputs.digest }}
21 | steps:
22 | - name: 检出 Git 仓库
23 | uses: actions/checkout@v4
24 | with:
25 | fetch-depth: 0
26 |
27 | - name: 克隆 maim_message
28 | uses: actions/checkout@v4
29 | with:
30 | repository: MaiM-with-u/maim_message
31 | path: maim_message
32 |
33 | - name: 克隆 MaiMBot-LPMM
34 | uses: actions/checkout@v4
35 | with:
36 | repository: MaiM-with-u/MaiMBot-LPMM
37 | path: MaiMBot-LPMM
38 |
39 | - name: 设置 Docker Buildx
40 | uses: docker/setup-buildx-action@v3
41 | with:
42 | buildkitd-flags: --debug
43 |
44 | - name: 登录到 Docker Hub
45 | uses: docker/login-action@v3
46 | with:
47 | username: ${{ secrets.DOCKERHUB_USERNAME }}
48 | password: ${{ secrets.DOCKERHUB_TOKEN }}
49 |
50 | - name: Docker 元数据
51 | id: meta
52 | uses: docker/metadata-action@v5
53 | with:
54 | images: ${{ secrets.DOCKERHUB_USERNAME }}/maibot
55 |
56 | - name: 动态生成镜像标签
57 | id: tag
58 | run: |
59 | if [ "$GITHUB_REF" == "refs/heads/master" ]; then
60 | echo "tag=latest" >> $GITHUB_ENV
61 | elif [ "$GITHUB_REF" == "refs/heads/develop" ]; then
62 | echo "tag=dev" >> $GITHUB_ENV
63 | else
64 | echo "tag=${{ github.ref_name }}" >> $GITHUB_ENV
65 | fi
66 |
67 | - name: 构建并推送 AMD64 镜像
68 | id: build
69 | uses: docker/build-push-action@v5
70 | with:
71 | context: .
72 | platforms: linux/amd64
73 | labels: ${{ steps.meta.outputs.labels }}
74 | file: ./Dockerfile
75 | cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maibot:amd64-buildcache
76 | cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maibot:amd64-buildcache,mode=max
77 | outputs: type=image,name=${{ secrets.DOCKERHUB_USERNAME }}/maibot:${{ env.tag }},name-canonical=true,push=true
78 | build-args: |
79 | BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
80 | VCS_REF=${{ github.sha }}
81 | BRANCH_NAME=${{ github.ref_name }}
82 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.13.5-slim-bookworm
2 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
3 |
4 | # 工作目录
5 | WORKDIR /mmc
6 |
7 | # 复制依赖列表
8 | COPY requirements.txt .
9 | # 同级目录下需要有 maim_message MaiMBot-LPMM
10 | #COPY maim_message /maim_message
11 | COPY MaiMBot-LPMM /MaiMBot-LPMM
12 |
13 | # 编译器
14 | RUN apt-get update && apt-get install -y build-essential
15 |
16 | # lpmm编译安装
17 | RUN cd /MaiMBot-LPMM && uv pip install --system -r requirements.txt
18 | RUN uv pip install --system Cython py-cpuinfo setuptools
19 | RUN cd /MaiMBot-LPMM/lib/quick_algo && python build_lib.py --cleanup --cythonize --install
20 |
21 |
22 | # 安装依赖
23 | RUN uv pip install --system --upgrade pip
24 | #RUN uv pip install --system -e /maim_message
25 | RUN uv pip install --system -r requirements.txt
26 |
27 | # 复制项目代码
28 | COPY . .
29 |
30 | EXPOSE 8000
31 |
32 | ENTRYPOINT [ "python","bot.py" ]
--------------------------------------------------------------------------------
/PRIVACY.md:
--------------------------------------------------------------------------------
1 | ### MoFox_Bot 隐私说明
2 |
3 | **版本:** 1.1
4 | **更新于: 2025年8月30日**
5 | **生效日期:2025年8月26日**
6 | **适用的MoFox_Bot版本号:所有版本**
7 |
8 | **2025© MoFox_Bot项目团队*
9 |
10 | 我们(MoFox_Bot 项目团队)非常重视您的隐私。当您使用 MoFox_Bot 时,代表您同意我们按照本说明处理您的信息。
11 |
12 | ---
13 |
14 | #### 我们如何处理您的信息?
15 |
16 | 1. **对话内容与第三方服务**
17 | * 为了让机器人回复您,我们会将您的对话内容发送给第三方 AI 服务(比如 OpenAI、Google 等)。
18 | * 因此,您的对话会同时受到我们的隐私说明和第三方服务隐私政策的约束。
19 |
20 | 2. **本地知识库与记忆**
21 | * 我们会在您自己的设备上,利用您的对话内容来构建专属的知识库和记忆库。
22 | * 这样做能让机器人的回复更贴切、更连贯。这些数据**只会存储在您的本地**。
23 |
24 | 3. **本地日志文件**
25 | * 我们会在您的设备上记录程序运行日志,用于排查问题。这些日志**不会上传到网络**。
26 | * 只有当您需要技术支持并主动将日志文件发送给我们时,我们才会看到这些信息。
27 |
28 | ---
29 |
30 | #### 关于第三方插件
31 |
32 | * **请注意:** 您可以为 MoFox_Bot 安装由其他开发者制作的“插件”。
33 | * 这些插件可能会收集、处理或发送您的数据。**它们的行为由插件开发者决定,我们无法控制。**
34 | * 我们无法保证第三方插件的安全性。在使用前,请务必了解其独立的隐私政策。
35 | * **因使用第三方插件导致的任何隐私风险,需要您自行承担。**
36 |
37 | ---
38 |
39 | #### 责任声明
40 |
41 | * 如果因为您自己的操作、或者其他不可抗力因素导致您的信息泄露,您需要自行承担后果,我们不承担责任。
42 | * **再次强调:** 对于因使用**第三方插件**而产生的任何隐私或数据安全问题,我们概不负责。
43 |
44 | ---
45 |
46 | #### 条款更新
47 |
48 | * 我们将来可能会更新本隐私说明。如果我们更新了,您继续使用就代表您同意新的条款。如果您不同意,请停止使用。
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | - [x] 添加向适配器发送命令并获取返回的函数
2 | - [x] 适配器正向/反向Websocket支持
3 | - [x] 内置网页搜索及URL解析工具(多平台,轮询秘钥)
4 | - [x] 内置空间插件
5 | - [ ] 在好友聊天生成回复时设置输入状态
6 | - [x] 基于关键帧的视频识别功能
7 | - [x] 对XML,JSON等特殊消息解析
8 | - [x] 插件热重载
9 | - [x] 适配器黑/白名单迁移至独立配置文件,并支持热重载
10 | - [x] 添加MySQL支持,重构数据库
11 | - [x] 戳一戳插件支持
12 | - [x] 设置表情插件支持
13 | - [ ] 沙盒插件支持
14 | - [ ] 点赞插件支持
15 | - [x] 真艾特支持(LLM决定是否艾特)
16 | - [x] 重新支持聊天模式配置
17 | - [ ] 好友强制回复支持
18 | - [x] 添加表情包情感分析功能
19 | - [x] 添加主动思考配置
20 | - [x] 添加日程管理
21 | - [x] 添加MCP SSE支持
22 | - [ ] 增加基于GPT-Sovits的多情感语音合成功能(插件形式)
23 | - [ ] 增加基于Open Voice的语音合成功能(插件形式)
24 | - [x] 对聊天信息的视频增加一个videoid(就像imageid一样)
25 | - [ ] 修复generate_responce_for_image方法有的时候会对同一张图片生成两次描述的问题
26 | - [x] 主动思考的通用提示词改进
27 | - [x] 添加贴表情聊天流判断,过滤好友
28 |
29 |
30 | - 大工程
31 | · 增加一个基于Rust后端,daisyui为(装饰的)前端的启动器,以下是详细功能
32 | - 一个好看的ui
33 | - 支持所有的非core的插件管理
34 | - 支持一键部署所有环境
35 | - 将启动时的powershell界面内嵌到前端启动器内
36 | - 能够支持自由修改bot、llm的配置
37 | - 兼容Matcha,将Matcha的界面也嵌入到启动器内
38 | - 数据库预览以及修改功能
39 | - (待确定)Live 2d chat功能的开发
--------------------------------------------------------------------------------
/__main__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """Bot项目的主入口点"""
3 |
4 | if __name__ == "__main__":
5 | # 设置Python路径并执行bot.py
6 | import sys
7 | from pathlib import Path
8 |
9 | # 添加当前目录到Python路径
10 | current_dir = Path(__file__).parent
11 | sys.path.insert(0, str(current_dir))
12 |
13 | # 执行bot.py的代码
14 | bot_file = current_dir / "bot.py"
15 | with open(bot_file, encoding="utf-8") as f:
16 | exec(f.read())
17 |
18 |
19 | # 这个文件是为了适配一键包使用的,在一键包项目之外没有用
20 |
--------------------------------------------------------------------------------
/changelogs/changes.md:
--------------------------------------------------------------------------------
1 | # 插件API与规范修改
2 |
3 | 1. 现在`plugin_system`的`__init__.py`文件中包含了所有插件API的导入,用户可以直接使用`from src.plugin_system import *`来导入所有API。
4 |
5 | 2. register_plugin函数现在转移到了`plugin_system.apis.plugin_register_api`模块中,用户可以通过`from src.plugin_system.apis.plugin_register_api import register_plugin`来导入。
6 | - 顺便一提,按照1中说法,你可以这么用:
7 | ```python
8 | from src.plugin_system import register_plugin
9 | ```
10 |
11 | 3. 现在强制要求的property如下,即你必须覆盖的属性有:
12 | - `plugin_name`: 插件名称,必须是唯一的。(与文件夹相同)
13 | - `enable_plugin`: 是否启用插件,默认为`True`。
14 | - `dependencies`: 插件依赖的其他插件列表,默认为空。**现在并不检查(也许)**
15 | - `python_dependencies`: 插件依赖的Python包列表,默认为空。**现在并不检查**
16 | - `config_file_name`: 插件配置文件名,默认为`config.toml`。
17 | - `config_schema`: 插件配置文件的schema,用于自动生成配置文件。
18 | 4. 部分API的参数类型和返回值进行了调整
19 | - `chat_api.py`中获取流的参数中可以使用一个特殊的枚举类型来获得所有平台的 ChatStream 了。
20 | - `config_api.py`中的`get_global_config`和`get_plugin_config`方法现在支持嵌套访问的配置键名。
21 | - `database_api.py`中的`db_query`方法调整了参数顺序以增强参数限制的同时,保证了typing正确;`db_get`方法增加了`single_result`参数,与`db_query`保持一致。
22 | 5. 增加了`logging_api`,可以用`get_logger`来获取日志记录器。
23 | 6. 增加了插件和组件管理的API。
24 | 7. `BaseCommand`的`execute`方法现在返回一个三元组,包含是否执行成功、可选的回复消息和是否拦截消息。
25 | - 这意味着你终于可以动态控制是否继续后续消息的处理了。
26 | 8. 移除了dependency_manager,但是依然保留了`python_dependencies`属性,等待后续重构。
27 | - 一并移除了文档有关manager的内容。
28 | 9. 增加了工具的有关api
29 |
30 | # 插件系统修改
31 | 1. 现在所有的匹配模式不再是关键字了,而是枚举类。**(可能有遗漏)**
32 | 2. 修复了一下显示插件信息不显示的问题。同时精简了一下显示内容
33 | 3. 修复了插件系统混用了`plugin_name`和`display_name`的问题。现在所有的插件信息都使用`display_name`来显示,而内部标识仍然使用`plugin_name`。
34 | 4. 现在增加了参数类型检查,完善了对应注释
35 | 5. 现在插件抽象出了总基类 `PluginBase`
36 | - 基于`Action`和`Command`的插件基类现在为`BasePlugin`。
37 | - 基于`Event`的插件基类现在为`BaseEventPlugin`。
38 | - 基于`Action`,`Command`和`Event`的插件基类现在为`BasePlugin`,所有插件都应该继承此基类。
39 | - `BasePlugin`继承自`PluginBase`。
40 | - 所有的插件类都由`register_plugin`装饰器注册。
41 | 6. 现在我们终于可以让插件有自定义的名字了!
42 | - 真正实现了插件的`plugin_name`**不受文件夹名称限制**的功能。(吐槽:可乐你的某个小小细节导致我搞了好久……)
43 | - 通过在插件类中定义`plugin_name`属性来指定插件内部标识符。
44 | - 由于此更改一个文件中现在可以有多个插件类,但每个插件类必须有**唯一的**`plugin_name`。
45 | - 在某些插件加载失败时,现在会显示包名而不是插件内部标识符。
46 | - 例如:`MaiMBot.plugins.example_plugin`而不是`example_plugin`。
47 | - 仅在插件 import 失败时会如此,正常注册过程中失败的插件不会显示包名,而是显示插件内部标识符。(这是特性,但是基本上不可能出现这个情况)
48 | 7. 现在不支持单文件插件了,加载方式已经完全删除。
49 | 8. 把`BaseEventPlugin`合并到了`BasePlugin`中,所有插件都应该继承自`BasePlugin`。
50 | 9. `BaseEventHandler`现在有了`get_config`方法了。
51 | 10. 修正了`main.py`中的错误输出。
52 | 11. 修正了`command`所编译的`Pattern`注册时的错误输出。
53 | 12. `events_manager`有了task相关逻辑了。
54 | 13. 现在有了插件卸载和重载功能了,也就是热插拔。
55 | 14. 实现了组件的全局启用和禁用功能。
56 | - 通过`enable_component`和`disable_component`方法来启用或禁用组件。
57 | - 不过这个操作不会保存到配置文件~
58 | 15. 实现了组件的局部禁用,也就是针对某一个聊天禁用的功能。
59 | - 通过`disable_specific_chat_action`,`enable_specific_chat_action`,`disable_specific_chat_command`,`enable_specific_chat_command`,`disable_specific_chat_event_handler`,`enable_specific_chat_event_handler`来操作
60 | - 同样不保存到配置文件~
61 | 16. 把`BaseTool`一并合并进入了插件系统
62 |
63 | # 官方插件修改
64 | 1. `HelloWorld`插件现在有一个样例的`EventHandler`。
65 | 2. 内置插件增加了一个通过`Command`来管理插件的功能。具体是使用`/pm`命令唤起。(需要自行启用)
66 | 3. `HelloWorld`插件现在有一个样例的`CompareNumbersTool`。
67 |
68 | ### 执笔BGM
69 | 塞壬唱片!
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | adapters:
3 | container_name: maim-bot-adapters
4 | #### prod ####
5 | image: unclas/maimbot-adapter:latest
6 | # image: infinitycat/maimbot-adapter:latest
7 | #### dev ####
8 | # image: unclas/maimbot-adapter:dev
9 | # image: infinitycat/maimbot-adapter:dev
10 | environment:
11 | - TZ=Asia/Shanghai
12 | # ports:
13 | # - "8095:8095"
14 | volumes:
15 | - ./docker-config/adapters/config.toml:/adapters/config.toml # 持久化adapters配置文件
16 | - ./data/adapters:/adapters/data # adapters 数据持久化
17 | restart: always
18 | networks:
19 | - maim_bot
20 | core:
21 | container_name: maim-bot-core
22 | #### prod ####
23 | image: sengokucola/maibot:latest
24 | # image: infinitycat/maibot:latest
25 | #### dev ####
26 | # image: sengokucola/maibot:dev
27 | # image: infinitycat/maibot:dev
28 | environment:
29 | - TZ=Asia/Shanghai
30 | # - EULA_AGREE=99f08e0cab0190de853cb6af7d64d4de # 同意EULA
31 | # - PRIVACY_AGREE=9943b855e72199d0f5016ea39052f1b6 # 同意EULA
32 | # ports:
33 | # - "8000:8000"
34 | volumes:
35 | - ./docker-config/mmc/.env:/MaiMBot/.env # 持久化env配置文件
36 | - ./docker-config/mmc:/MaiMBot/config # 持久化bot配置文件
37 | - ./data/MaiMBot/maibot_statistics.html:/MaiMBot/maibot_statistics.html #统计数据输出
38 | - ./data/MaiMBot:/MaiMBot/data # 共享目录
39 | - ./data/MaiMBot/plugins:/MaiMBot/plugins # 插件目录
40 | - ./data/MaiMBot/logs:/MaiMBot/logs # 日志目录
41 | - site-packages:/usr/local/lib/python3.13/site-packages # 持久化Python包
42 | restart: always
43 | networks:
44 | - maim_bot
45 | napcat:
46 | environment:
47 | - NAPCAT_UID=1000
48 | - NAPCAT_GID=1000
49 | - TZ=Asia/Shanghai
50 | ports:
51 | - "6099:6099"
52 | volumes:
53 | - ./docker-config/napcat:/app/napcat/config # 持久化napcat配置文件
54 | - ./data/qq:/app/.config/QQ # 持久化QQ本体
55 | - ./data/MaiMBot:/MaiMBot/data # 共享目录
56 | container_name: maim-bot-napcat
57 | restart: always
58 | image: mlikiowa/napcat-docker:latest
59 | networks:
60 | - maim_bot
61 | sqlite-web:
62 | # 注意:coleifer/sqlite-web 镜像不支持arm64
63 | image: coleifer/sqlite-web
64 | container_name: sqlite-web
65 | restart: always
66 | ports:
67 | - "8120:8080"
68 | volumes:
69 | - ./data/MaiMBot:/data/MaiMBot
70 | environment:
71 | - SQLITE_DATABASE=MaiMBot/MaiBot.db # 你的数据库文件
72 | networks:
73 | - maim_bot
74 |
75 | # chat2db占用相对较高但是功能强大
76 | # 内存占用约600m,内存充足推荐选此
77 | # chat2db:
78 | # image: chat2db/chat2db:latest
79 | # container_name: maim-bot-chat2db
80 | # restart: always
81 | # ports:
82 | # - "10824:10824"
83 | # volumes:
84 | # - ./data/MaiMBot:/data/MaiMBot
85 | # networks:
86 | # - maim_bot
87 | volumes:
88 | site-packages:
89 | networks:
90 | maim_bot:
91 | driver: bridge
92 |
--------------------------------------------------------------------------------
/docs/assets/1750326700269.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoFox-Studio/MoFox_Bot/3f9b3509d3c7d6fa4ea3ae40fdcb4b53b3196450/docs/assets/1750326700269.png
--------------------------------------------------------------------------------
/docs/assets/1750332508760.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoFox-Studio/MoFox_Bot/3f9b3509d3c7d6fa4ea3ae40fdcb4b53b3196450/docs/assets/1750332508760.png
--------------------------------------------------------------------------------
/docs/assets/image-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoFox-Studio/MoFox_Bot/3f9b3509d3c7d6fa4ea3ae40fdcb4b53b3196450/docs/assets/image-1.png
--------------------------------------------------------------------------------
/docs/assets/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoFox-Studio/MoFox_Bot/3f9b3509d3c7d6fa4ea3ae40fdcb4b53b3196450/docs/assets/image.png
--------------------------------------------------------------------------------
/docs/deployment_guide.md:
--------------------------------------------------------------------------------
1 | # MoFox_Bot 部署指南
2 |
3 | 欢迎使用 MoFox_Bot!本指南将引导您完成在 Windows 环境下部署 MoFox_Bot 的全部过程。
4 |
5 | ## 1. 系统要求
6 |
7 | - **操作系统**: Windows 10 或 Windows 11
8 | - **Python**: 版本 >= 3.10
9 | - **Git**: 用于克隆项目仓库
10 | - **uv**: 推荐的 Python 包管理器 (版本 >= 0.1.0)
11 |
12 | ## 2. 部署步骤
13 |
14 | ### 第一步:获取必要的文件
15 |
16 | 首先,创建一个用于存放 MoFox_Bot 相关文件的文件夹,并通过 `git` 克隆 MoFox_Bot 主程序和 Napcat 适配器。
17 |
18 | ```shell
19 | mkdir MoFox_Bot_Deployment
20 | cd MoFox_Bot_Deployment
21 | git clone hhttps://github.com/MoFox-Studio/MoFox_Bot.git
22 | git clone https://github.com/MoFox-Studio/Napcat-Adapter.git
23 | ```
24 |
25 | ### 第二步:环境配置
26 |
27 | 我们推荐使用 `uv` 来管理 Python 环境和依赖,因为它提供了更快的安装速度和更好的依赖管理体验。
28 |
29 | **安装 uv:**
30 |
31 | ```shell
32 | pip install uv
33 | ```
34 |
35 | ### 第三步:依赖安装
36 |
37 | **1. 安装 MoFox_Bot 依赖:**
38 |
39 | 进入 `mmc` 文件夹,创建虚拟环境并安装依赖。
40 |
41 | ```shell
42 | cd mmc
43 | uv venv
44 | uv pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple --upgrade
45 | ```
46 |
47 | **2. 安装 Napcat-Adapter 依赖:**
48 |
49 | 回到上一级目录,进入 `Napcat-Adapter` 文件夹,创建虚拟环境并安装依赖。
50 |
51 | ```shell
52 | cd ..
53 | cd Napcat-Adapter
54 | uv venv
55 | uv pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple --upgrade
56 | ```
57 |
58 | ### 第四步:配置 MoFox_Bot 和 Adapter
59 |
60 | **1. MoFox_Bot 配置:**
61 |
62 | - 在 `mmc` 文件夹中,将 `template/bot_config_template.toml` 复制到 `config/bot_config.toml`。
63 | - 将 `template/model_config_template.toml` 复制到 `config/model_config.toml`。
64 | - 根据 [模型配置指南](guides/model_configuration_guide.md) 和 `bot_config.toml` 文件中的注释,填写您的 API Key 和其他相关配置。
65 |
66 | **2. Napcat-Adapter 配置:**
67 |
68 | - 在 `Napcat-Adapter` 文件夹中,将 `template/template_config.toml` 复制到根目录并改名为 `config.toml`。
69 | - 打开 `config.toml` 文件,配置 `[Napcat_Server]` 和 `[MaiBot_Server]` 字段。
70 | - `[Napcat_Server]` 的 `port` 应与 Napcat 设置的反向代理 URL 中的端口相同。
71 | - `[MaiBot_Server]` 的 `port` 应与 MoFox_Bot 的 `bot_config.toml` 中设置的端口相同。
72 |
73 | ### 第五步:运行
74 |
75 | **1. 启动 Napcat:**
76 |
77 | 请参考 [NapCatQQ 文档](https://napcat-qq.github.io/) 进行部署和启动。
78 |
79 | **2. 启动 MoFox_Bot:**
80 |
81 | 进入 `mmc` 文件夹,使用 `uv` 运行。
82 |
83 | ```shell
84 | cd mmc
85 | uv run python bot.py
86 | ```
87 |
88 | **3. 启动 Napcat-Adapter:**
89 |
90 | 打开一个新的终端窗口,进入 `Napcat-Adapter` 文件夹,使用 `uv` 运行。
91 |
92 | ```shell
93 | cd Napcat-Adapter
94 | uv run python main.py
95 | ```
96 |
97 | 至此,MoFox_Bot 已成功部署并运行。
98 |
99 | ## 3. 详细配置说明
100 |
101 | ### `bot_config.toml`
102 |
103 | 这是 MoFox_Bot 的主配置文件,包含了机器人昵称、主人QQ、命令前缀、数据库设置等。请根据文件内的注释进行详细配置。
104 |
105 | ### `model_config.toml`
106 |
107 | 此文件用于配置 AI 模型和 API 服务提供商。详细配置方法请参考 [模型配置指南](guides/model_configuration_guide.md)。
108 |
109 | ### 插件配置
110 |
111 | 每个插件都有独立的配置文件,位于 `mmc/config/plugins/` 目录下。插件的配置由其 `config_schema` 自动生成。详细信息请参考 [插件配置完整指南](plugins/configuration-guide.md)。
112 |
113 | ## 4. 故障排除
114 |
115 | - **依赖安装失败**:
116 | - 尝试更换 PyPI 镜像源。
117 | - 检查网络连接。
118 | - **API 调用失败**:
119 | - 检查 `model_config.toml` 中的 API Key 和 `base_url` 是否正确。
120 | - **无法连接到 Napcat**:
121 | - 检查 Napcat 是否正常运行。
122 | - 确认 `Napcat-Adapter` 的 `config.toml` 中 `[Napcat_Server]` 的 `port` 是否与 Napcat 设置的端口一致。
123 |
124 | 如果遇到其他问题,请查看 `logs/` 目录下的日志文件以获取详细的错误信息。
--------------------------------------------------------------------------------
/docs/development/CONTRIBUTE.md:
--------------------------------------------------------------------------------
1 | # 如何给MoFox_Bot做贡献v1.0
2 |
3 | 修改时间2025/4/5
4 |
5 | 如有修改建议或疑问,请在github上建立issue
6 |
7 | 首先,非常感谢你抽出时间来做贡献!❤️
8 |
9 | 这份文档是告诉你,当你想向MoFox_Bot提交代码,或者想要以其他形式加入MoFox_Bot的开发,或周边插件的开发,你可以怎么做。
10 |
11 | 我们鼓励并重视任何形式的贡献,但无序的贡献只会使麦麦的维护与更新变成一团糟。因此,我们建议您在做出贡献之前,先查看本指南。
12 |
13 |
14 | > 另外,如果您喜欢这个项目,但只是没有时间做贡献,那也没关系。还有其他简单的方式来支持本项目并表达您的感激之情,我们也会非常高兴:
15 | > - 给我们点一颗小星星(Star)
16 | > - 在您的项目的readme中引用这个项目
17 |
18 | ## 目录
19 |
20 | ● [我有问题](#我有问题)
21 | ● [我想做贡献](#我想做贡献)
22 | ● [我想提出建议](#提出建议)
23 |
24 | ## 我有问题
25 |
26 | > 如果你想问一个问题,我们会假设你已经阅读了现有的文档。
27 |
28 | 在你提问之前,最好先搜索现有的[issue](/issues),看看是否有助于解决你的问题。如果你找到了匹配的issue,但仍需要追加说明,你可以在该issue下提出你的问题。同时,我们还建议你先在互联网上搜索答案。
29 |
30 | 如果你仍然觉得有必要提问并需要进行说明,我们建议你:
31 |
32 | ● 开一个[新Issue](/issues/new)。并尽可能详细地描述你的问题。
33 |
34 | ● 提供尽可能多的上下文信息,让我们更好地理解你遇到的问题。比如:提供版本信息(哪个分支,版本号是多少,运行环境有哪些等),具体取决于你认为相关的内容。
35 |
36 | 只要你提出的issue明确且合理,我们都会回复并尝试解决您的问题。
37 |
38 |
39 | ## 我想做贡献
40 |
41 | > ### 项目所有权与维护
42 | > MoFox_Bot项目(基于MaiBot修改,核心为MaiCore)由雅诺狐创建,采用GPL3开源协议。
43 | > MoFox_Bot项目现已移动至MoFox_Bot组织下,目前主要内容由核心开发组维护,整体由核心开发组、reviewer和所有贡献者共同维护(该部分在未来将会明确)。
44 | > 为了保证设计的统一和节省不必要的精力,以及为了对项目有整体的把控,我们对不同类型的贡献采取不同的审核策略:
45 | >
46 | > #### 功能新增
47 | > - 定义:涉及新功能添加、架构调整、重要模块重构等
48 | > - 要求:原则上暂不接收,你可以发布issue提供新功能建议。
49 | >
50 | > #### Bug修复
51 | > - 定义:修复现有功能中的错误,包括非预期行为(需要发布issue进行确认)和运行错误,不涉及新功能或架构变动
52 | > - 要求:由核心组成员或2名及以上reviewer同时确认才会被合并
53 | > - 关闭:包含预期行为改动,新功能,破坏原有功能,数据库破坏性改动等的pr将会被关闭
54 | >
55 | > #### 文档修补
56 | > - 定义:修复现有文档中的错误,提供新的帮助文档
57 | > - 要求:现需要提交至组织下docs仓库,由reviewer确认后合并
58 |
59 |
60 | > ### 法律声明
61 | > 当你为本项目贡献代码/文档时,你必须确认:
62 | > 1. 你贡献的内容100%是由你创作;
63 | > 2. 你对这些内容拥有相应的权利;
64 | > 3. 你贡献的内容将按项目许可协议使用。
65 |
66 |
67 | ## 提出建议
68 |
69 | 这一部分指导您如何为MoFox_Bot提交一个建议,包括全新的功能和对现有功能的小改进。遵循这些指南将有助于维护人员和社区了解您的建议并找到相关的建议。
70 |
71 | 在提交建议之前
72 |
73 | ● 请确保您正在使用最新版本(正式版请查看master分支,测试版查看dev分支)。
74 |
75 | ● 请确保您已经阅读了文档,以确认您的建议是否已经被实现,也许是通过单独的配置。
76 |
77 | ● 仔细阅读文档并了解项目目前是否支持该功能,也许可以通过单独的配置来实现。
78 |
79 | ● 进行一番[搜索](/issues)以查看是否已经有人提出了这个建议。如果有,请在现有的issue下添加评论,而不是新开一个issue。
80 |
81 | ● 请确保您的建议符合项目的范围和目标。你需要提出一个强有力的理由来说服项目的开发者这个功能的优点。请记住,我们希望的功能是对大多数用户有用的,而不仅仅是少数用户。如果你只是针对少数用户,请考虑编写一个插件。
82 |
83 | ### 附(暂定):
84 | 核心组成员:@foxcyber907(雅诺狐) @Furina-1013-create(Navinatte)@tt-P607(言柒)@ikun-11451(ikun两年半)@minecraft1024a(一闪)
85 |
86 | reviewer:核心组+MoFox_Bot主仓库合作者/权限者
87 |
88 | 贡献者:所有提交过贡献的用户
89 |
--------------------------------------------------------------------------------
/docs/integrations/Bing.md:
--------------------------------------------------------------------------------
1 | - **参数化与动态调整聊天行为**:
2 | - 将 `NormalChatInstance` 和 `HeartFlowChatInstance` 中的关键行为参数(例如:回复概率、思考频率、兴趣度阈值、状态转换条件等)提取出来,使其更易于配置。
3 | - 允许每个 `SubHeartflow` (即每个聊天场景) 拥有其独立的参数配置,实现"千群千面"。
4 | - 开发机制,使得这些参数能够被动态调整:
5 | - 基于外部反馈:例如,根据用户评价("话太多"或"太冷淡")调整回复频率。
6 | - 基于环境分析:例如,根据群消息的活跃度自动调整参与度。
7 | - 基于学习:通过分析历史交互数据,优化特定群聊下的行为模式。
8 | - 目标是让 Mai 在不同群聊中展现出更适应环境、更个性化的交互风格。
9 |
10 | - **动态 Prompt 生成与人格塑造**:
11 | - 当前 Prompt (提示词) 相对静态。计划实现动态或半结构化的 Prompt 生成。
12 | - Prompt 内容可根据以下因素调整:
13 | - **人格特质**: 通过参数化配置(如友善度、严谨性等),影响 Prompt 的措辞、语气和思考倾向,塑造更稳定和独特的人格。
14 | - **当前情绪**: 将实时情绪状态融入 Prompt,使回复更符合当下心境。
15 | - 目标:提升 `HeartFlowChatInstance` (HFC) 回复的多样性、一致性和真实感。
16 | - 前置:需要重构 Prompt 构建逻辑,可能引入 `PromptBuilder` 并提供标准接口 (认为是必须步骤)。
17 |
18 |
19 | - **增强工具调用能力 (Enhanced Tool Usage)**:
20 | - 扩展 `HeartFlowChatInstance` (HFC) 可用的工具集。
21 | - 考虑引入"元工具"或分层工具机制,允许 HFC 在需要时(如深度思考)访问更强大的工具,例如:
22 | - 修改自身或其他 `SubHeartflow` 的聊天参数。
23 | - 请求改变 Mai 的全局状态 (`MaiState`)。
24 | - 管理日程或执行更复杂的分析任务。
25 | - 目标:提升 HFC 的自主决策和行动能力,即使会增加一定的延迟。
26 |
27 | - **标准化人设生成 (Standardized Persona Generation)**:
28 | - **目标**: 解决手动配置 `人设` 文件缺乏标准、难以全面描述个性的问题,并生成更丰富、可操作的人格资源。
29 | - **方法**: 利用大型语言模型 (LLM) 辅助生成标准化的、结构化的人格**资源包**。
30 | - **生成内容**: 不仅生成描述性文本(替代现有 `individual` 配置),还可以同时生成与该人格配套的:
31 | - **相关工具 (Tools)**: 该人格倾向于使用的工具或能力。
32 | - **初始记忆/知识库 (Memories/Knowledge)**: 定义其背景和知识基础。
33 | - **核心行为模式 (Core Behavior Patterns)**: 预置一些典型的行为方式,可作为行为学习的起点。
34 | - **实现途径**:
35 | - 通过与 LLM 的交互式对话来定义和细化人格及其配套资源。
36 | - 让 LLM 分析提供的文本材料(如小说、背景故事)来提取人格特质和相关信息。
37 | - **优势**: 替代易出错且标准不一的手动配置,生成更丰富、一致、包含配套资源且易于系统理解和应用的人格包。
38 |
39 |
40 | - **探索高级记忆检索机制 (GE 系统概念):**
41 | - 研究超越简单关键词/近期性检索的记忆模型。
42 | - 考虑引入基于事件关联、相对时间线索和绝对时间锚点的检索方式。
43 | - 可能涉及设计新的事件表示或记忆结构。
44 |
45 | - **基于人格生成预设知识:**
46 | - 开发利用 LLM 和人格配置生成背景知识的功能。
47 | - 这些知识应符合角色的行为风格和可能的经历。
48 | - 作为一种"冷启动"或丰富角色深度的方式。
49 |
50 |
51 | 1.更nb的工作记忆,直接开一个play_ground,通过llm进行内容检索,这个play_ground可以容纳巨量信息,并且十分通用化,十分好。
--------------------------------------------------------------------------------
/docs/plugins/api/chat-api.md:
--------------------------------------------------------------------------------
1 | # 聊天API
2 |
3 | 聊天API模块专门负责聊天信息的查询和管理,帮助插件获取和管理不同的聊天流。
4 |
5 | ## 导入方式
6 |
7 | ```python
8 | from src.plugin_system import chat_api
9 | # 或者
10 | from src.plugin_system.apis import chat_api
11 | ```
12 |
13 | 一种**Deprecated**方式:
14 | ```python
15 | from src.plugin_system.apis.chat_api import ChatManager
16 | ```
17 |
18 | ## 主要功能
19 |
20 | ### 1. 获取所有的聊天流
21 |
22 | ```python
23 | def get_all_streams(platform: Optional[str] | SpecialTypes = "qq") -> List[ChatStream]:
24 | ```
25 |
26 | **Args**:
27 | - `platform`:平台筛选,默认为"qq",可以使用`SpecialTypes`枚举类中的`SpecialTypes.ALL_PLATFORMS`来获取所有平台的聊天流。
28 |
29 | **Returns**:
30 | - `List[ChatStream]`:聊天流列表
31 |
32 | ### 2. 获取群聊聊天流
33 |
34 | ```python
35 | def get_group_streams(platform: Optional[str] | SpecialTypes = "qq") -> List[ChatStream]:
36 | ```
37 |
38 | **Args**:
39 | - `platform`:平台筛选,默认为"qq",可以使用`SpecialTypes`枚举类中的`SpecialTypes.ALL_PLATFORMS`来获取所有平台的群聊流。
40 |
41 | **Returns**:
42 | - `List[ChatStream]`:群聊聊天流列表
43 |
44 | ### 3. 获取私聊聊天流
45 |
46 | ```python
47 | def get_private_streams(platform: Optional[str] | SpecialTypes = "qq") -> List[ChatStream]:
48 | ```
49 |
50 | **Args**:
51 | - `platform`:平台筛选,默认为"qq",可以使用`SpecialTypes`枚举类中的`SpecialTypes.ALL_PLATFORMS`来获取所有平台的私聊流。
52 |
53 | **Returns**:
54 | - `List[ChatStream]`:私聊聊天流列表
55 |
56 | ### 4. 根据群ID获取聊天流
57 |
58 | ```python
59 | def get_stream_by_group_id(group_id: str, platform: Optional[str] | SpecialTypes = "qq") -> Optional[ChatStream]:
60 | ```
61 |
62 | **Args**:
63 | - `group_id`:群聊ID
64 | - `platform`:平台筛选,默认为"qq",可以使用`SpecialTypes`枚举类中的`SpecialTypes.ALL_PLATFORMS`来获取所有平台的群聊流。
65 |
66 | **Returns**:
67 | - `Optional[ChatStream]`:聊天流对象,如果未找到返回None
68 |
69 | ### 5. 根据用户ID获取私聊流
70 |
71 | ```python
72 | def get_stream_by_user_id(user_id: str, platform: Optional[str] | SpecialTypes = "qq") -> Optional[ChatStream]:
73 | ```
74 |
75 | **Args**:
76 | - `user_id`:用户ID
77 | - `platform`:平台筛选,默认为"qq",可以使用`SpecialTypes`枚举类中的`SpecialTypes.ALL_PLATFORMS`来获取所有平台的私聊流。
78 |
79 | **Returns**:
80 | - `Optional[ChatStream]`:聊天流对象,如果未找到返回None
81 |
82 | ### 6. 获取聊天流类型
83 |
84 | ```python
85 | def get_stream_type(chat_stream: ChatStream) -> str:
86 | ```
87 |
88 | **Args**:
89 | - `chat_stream`:聊天流对象
90 |
91 | **Returns**:
92 | - `str`:聊天流类型,可能的值包括`private`(私聊流),`group`(群聊流)以及`unknown`(未知类型)。
93 |
94 | ### 7. 获取聊天流信息
95 |
96 | ```python
97 | def get_stream_info(chat_stream: ChatStream) -> Dict[str, Any]:
98 | ```
99 |
100 | **Args**:
101 | - `chat_stream`:聊天流对象
102 |
103 | **Returns**:
104 | - `Dict[str, Any]`:聊天流的详细信息,包括但不限于:
105 | - `stream_id`:聊天流ID
106 | - `platform`:平台名称
107 | - `type`:聊天流类型
108 | - `group_id`:群聊ID
109 | - `group_name`:群聊名称
110 | - `user_id`:用户ID
111 | - `user_name`:用户名称
112 |
113 | ### 8. 获取聊天流统计摘要
114 |
115 | ```python
116 | def get_streams_summary() -> Dict[str, int]:
117 | ```
118 |
119 | **Returns**:
120 | - `Dict[str, int]`:聊天流统计信息摘要,包含以下键:
121 | - `total_streams`:总聊天流数量
122 | - `group_streams`:群聊流数量
123 | - `private_streams`:私聊流数量
124 | - `qq_streams`:QQ平台流数量
125 |
126 |
127 | ## 注意事项
128 |
129 | 1. 大部分函数在参数不合法时候会抛出异常,请确保你的程序进行了捕获。
130 | 2. `ChatStream`对象包含了聊天的完整信息,包括用户信息、群信息等。
--------------------------------------------------------------------------------
/docs/plugins/api/config-api.md:
--------------------------------------------------------------------------------
1 | # 配置API
2 |
3 | 配置API模块提供了配置读取功能,让插件能够安全地访问全局配置和插件配置。
4 |
5 | ## 导入方式
6 |
7 | ```python
8 | from src.plugin_system.apis import config_api
9 | # 或者
10 | from src.plugin_system import config_api
11 | ```
12 |
13 | ## 主要功能
14 |
15 | ### 1. 访问全局配置
16 |
17 | ```python
18 | def get_global_config(key: str, default: Any = None) -> Any:
19 | ```
20 |
21 | **Args**:
22 | - `key`: 命名空间式配置键名,使用嵌套访问,如 "section.subsection.key",大小写敏感
23 | - `default`: 如果配置不存在时返回的默认值
24 |
25 | **Returns**:
26 | - `Any`: 配置值或默认值
27 |
28 | #### 示例:
29 | 获取机器人昵称
30 | ```python
31 | bot_name = config_api.get_global_config("bot.nickname", "MaiBot")
32 | ```
33 |
34 | ### 2. 获取插件配置
35 |
36 | ```python
37 | def get_plugin_config(plugin_config: dict, key: str, default: Any = None) -> Any:
38 | ```
39 | **Args**:
40 | - `plugin_config`: 插件配置字典
41 | - `key`: 配置键名,支持嵌套访问如 "section.subsection.key",大小写敏感
42 | - `default`: 如果配置不存在时返回的默认值
43 |
44 | **Returns**:
45 | - `Any`: 配置值或默认值
46 |
47 | ## 注意事项
48 |
49 | 1. **只读访问**:配置API只提供读取功能,插件不能修改全局配置
50 | 2. **错误处理**:所有函数都有错误处理,失败时会记录日志并返回默认值
51 | 3. **安全性**:插件通过此API访问配置是安全和隔离的
52 | 4. **性能**:频繁访问的配置建议在插件初始化时获取并缓存
--------------------------------------------------------------------------------
/docs/plugins/api/llm-api.md:
--------------------------------------------------------------------------------
1 | # LLM API
2 |
3 | LLM API模块提供与大语言模型交互的功能,让插件能够使用系统配置的LLM模型进行内容生成。
4 |
5 | ## 导入方式
6 |
7 | ```python
8 | from src.plugin_system.apis import llm_api
9 | # 或者
10 | from src.plugin_system import llm_api
11 | ```
12 |
13 | ## 主要功能
14 |
15 | ### 1. 查询可用模型
16 | ```python
17 | def get_available_models() -> Dict[str, TaskConfig]:
18 | ```
19 | 获取所有可用的模型配置。
20 |
21 | **Return:**
22 | - `Dict[str, TaskConfig]`:模型配置字典,key为模型名称,value为模型配置对象。
23 |
24 | ### 2. 使用模型生成内容
25 | ```python
26 | async def generate_with_model(
27 | prompt: str,
28 | model_config: TaskConfig,
29 | request_type: str = "plugin.generate",
30 | temperature: Optional[float] = None,
31 | max_tokens: Optional[int] = None,
32 | ) -> Tuple[bool, str, str, str]:
33 | ```
34 | 使用指定模型生成内容。
35 |
36 | **Args:**
37 | - `prompt`:提示词。
38 | - `model_config`:模型配置对象(从 `get_available_models` 获取)。
39 | - `request_type`:请求类型标识,默认为 `"plugin.generate"`。
40 | - `temperature`:生成内容的温度设置,影响输出的随机性。
41 | - `max_tokens`:生成内容的最大token数。
42 |
43 | **Return:**
44 | - `Tuple[bool, str, str, str]`:返回一个元组,包含(是否成功, 生成的内容, 推理过程, 模型名称)。
45 |
46 | ### 3. 有Tool情况下使用模型生成内容
47 | ```python
48 | async def generate_with_model_with_tools(
49 | prompt: str,
50 | model_config: TaskConfig,
51 | tool_options: List[Dict[str, Any]] | None = None,
52 | request_type: str = "plugin.generate",
53 | temperature: Optional[float] = None,
54 | max_tokens: Optional[int] = None,
55 | ) -> Tuple[bool, str, str, str, List[ToolCall] | None]:
56 | ```
57 | 使用指定模型生成内容,并支持工具调用。
58 |
59 | **Args:**
60 | - `prompt`:提示词。
61 | - `model_config`:模型配置对象(从 `get_available_models` 获取)。
62 | - `tool_options`:工具选项列表,包含可用工具的配置,字典为每一个工具的定义,参见[tool-components.md](../tool-components.md#属性说明),可用`tool_api.get_llm_available_tool_definitions()`获取并选择。
63 | - `request_type`:请求类型标识,默认为 `"plugin.generate"`。
64 | - `temperature`:生成内容的温度设置,影响输出的随机性。
65 | - `max_tokens`:生成内容的最大token数。
--------------------------------------------------------------------------------
/docs/plugins/api/logging-api.md:
--------------------------------------------------------------------------------
1 | # Logging API
2 |
3 | Logging API模块提供了获取本体logger的功能,允许插件记录日志信息。
4 |
5 | ## 导入方式
6 |
7 | ```python
8 | from src.plugin_system.apis import get_logger
9 | # 或者
10 | from src.plugin_system import get_logger
11 | ```
12 |
13 | ## 主要功能
14 | ### 1. 获取本体logger
15 | ```python
16 | def get_logger(name: str) -> structlog.stdlib.BoundLogger:
17 | ```
18 | 获取本体logger实例。
19 |
20 | **Args:**
21 | - `name` (str): 日志记录器的名称。
22 |
23 | **Returns:**
24 | - 一个logger实例,有以下方法:
25 | - `debug`
26 | - `info`
27 | - `warning`
28 | - `error`
29 | - `critical`
--------------------------------------------------------------------------------
/docs/plugins/api/person-api.md:
--------------------------------------------------------------------------------
1 | # 个人信息API
2 |
3 | 个人信息API模块提供用户信息查询和管理功能,让插件能够获取和使用用户的相关信息。
4 |
5 | ## 导入方式
6 |
7 | ```python
8 | from src.plugin_system.apis import person_api
9 | # 或者
10 | from src.plugin_system import person_api
11 | ```
12 |
13 | ## 主要功能
14 |
15 | ### 1. Person ID 获取
16 | ```python
17 | def get_person_id(platform: str, user_id: int) -> str:
18 | ```
19 | 根据平台和用户ID获取person_id
20 |
21 | **Args:**
22 | - `platform`:平台名称,如 "qq", "telegram" 等
23 | - `user_id`:用户ID
24 |
25 | **Returns:**
26 | - `str`:唯一的person_id(MD5哈希值)
27 |
28 | #### 示例
29 | ```python
30 | person_id = person_api.get_person_id("qq", 123456)
31 | ```
32 |
33 | ### 2. 用户信息查询
34 | ```python
35 | async def get_person_value(person_id: str, field_name: str, default: Any = None) -> Any:
36 | ```
37 | 查询单个用户信息字段值
38 |
39 | **Args:**
40 | - `person_id`:用户的唯一标识ID
41 | - `field_name`:要获取的字段名
42 | - `default`:字段值不存在时的默认值
43 |
44 | **Returns:**
45 | - `Any`:字段值或默认值
46 |
47 | #### 示例
48 | ```python
49 | nickname = await person_api.get_person_value(person_id, "nickname", "未知用户")
50 | impression = await person_api.get_person_value(person_id, "impression")
51 | ```
52 |
53 | ### 3. 批量用户信息查询
54 | ```python
55 | async def get_person_values(person_id: str, field_names: list, default_dict: Optional[dict] = None) -> dict:
56 | ```
57 | 批量获取用户信息字段值
58 |
59 | **Args:**
60 | - `person_id`:用户的唯一标识ID
61 | - `field_names`:要获取的字段名列表
62 | - `default_dict`:默认值字典,键为字段名,值为默认值
63 |
64 | **Returns:**
65 | - `dict`:字段名到值的映射字典
66 |
67 | #### 示例
68 | ```python
69 | values = await person_api.get_person_values(
70 | person_id,
71 | ["nickname", "impression", "know_times"],
72 | {"nickname": "未知用户", "know_times": 0}
73 | )
74 | ```
75 |
76 | ### 4. 判断用户是否已知
77 | ```python
78 | async def is_person_known(platform: str, user_id: int) -> bool:
79 | ```
80 | 判断是否认识某个用户
81 |
82 | **Args:**
83 | - `platform`:平台名称
84 | - `user_id`:用户ID
85 |
86 | **Returns:**
87 | - `bool`:是否认识该用户
88 |
89 | ### 5. 根据用户名获取Person ID
90 | ```python
91 | def get_person_id_by_name(person_name: str) -> str:
92 | ```
93 | 根据用户名获取person_id
94 |
95 | **Args:**
96 | - `person_name`:用户名
97 |
98 | **Returns:**
99 | - `str`:person_id,如果未找到返回空字符串
100 |
101 | ## 常用字段说明
102 |
103 | ### 基础信息字段
104 | - `nickname`:用户昵称
105 | - `platform`:平台信息
106 | - `user_id`:用户ID
107 |
108 | ### 关系信息字段
109 | - `impression`:对用户的印象
110 | - `points`: 用户特征点
111 |
112 | 其他字段可以参考`PersonInfo`类的属性(位于`src.common.database.database_model`)
113 |
114 | ## 注意事项
115 |
116 | 1. **异步操作**:部分查询函数都是异步的,需要使用`await`
117 | 2. **性能考虑**:批量查询优于单个查询
118 | 3. **隐私保护**:确保用户信息的使用符合隐私政策
119 | 4. **数据一致性**:person_id是用户的唯一标识,应妥善保存和使用
--------------------------------------------------------------------------------
/docs/plugins/api/plugin-manage-api.md:
--------------------------------------------------------------------------------
1 | # 插件管理API
2 |
3 | 插件管理API模块提供了对插件的加载、卸载、重新加载以及目录管理功能。
4 |
5 | ## 导入方式
6 | ```python
7 | from src.plugin_system.apis import plugin_manage_api
8 | # 或者
9 | from src.plugin_system import plugin_manage_api
10 | ```
11 |
12 | ## 功能概述
13 |
14 | 插件管理API主要提供以下功能:
15 | - **插件查询** - 列出当前加载的插件或已注册的插件。
16 | - **插件管理** - 加载、卸载、重新加载插件。
17 | - **插件目录管理** - 添加插件目录并重新扫描。
18 |
19 | ## 主要功能
20 |
21 | ### 1. 列出当前加载的插件
22 | ```python
23 | def list_loaded_plugins() -> List[str]:
24 | ```
25 | 列出所有当前加载的插件。
26 |
27 | **Returns:**
28 | - `List[str]` - 当前加载的插件名称列表。
29 |
30 | ### 2. 列出所有已注册的插件
31 | ```python
32 | def list_registered_plugins() -> List[str]:
33 | ```
34 | 列出所有已注册的插件。
35 |
36 | **Returns:**
37 | - `List[str]` - 已注册的插件名称列表。
38 |
39 | ### 3. 获取插件路径
40 | ```python
41 | def get_plugin_path(plugin_name: str) -> str:
42 | ```
43 | 获取指定插件的路径。
44 |
45 | **Args:**
46 | - `plugin_name` (str): 要查询的插件名称。
47 | **Returns:**
48 | - `str` - 插件的路径,如果插件不存在则 raise ValueError。
49 |
50 | ### 4. 卸载指定的插件
51 | ```python
52 | async def remove_plugin(plugin_name: str) -> bool:
53 | ```
54 | 卸载指定的插件。
55 |
56 | **Args:**
57 | - `plugin_name` (str): 要卸载的插件名称。
58 |
59 | **Returns:**
60 | - `bool` - 卸载是否成功。
61 |
62 | ### 5. 重新加载指定的插件
63 | ```python
64 | async def reload_plugin(plugin_name: str) -> bool:
65 | ```
66 | 重新加载指定的插件。
67 |
68 | **Args:**
69 | - `plugin_name` (str): 要重新加载的插件名称。
70 |
71 | **Returns:**
72 | - `bool` - 重新加载是否成功。
73 |
74 | ### 6. 加载指定的插件
75 | ```python
76 | def load_plugin(plugin_name: str) -> Tuple[bool, int]:
77 | ```
78 | 加载指定的插件。
79 |
80 | **Args:**
81 | - `plugin_name` (str): 要加载的插件名称。
82 |
83 | **Returns:**
84 | - `Tuple[bool, int]` - 加载是否成功,成功或失败的个数。
85 |
86 | ### 7. 添加插件目录
87 | ```python
88 | def add_plugin_directory(plugin_directory: str) -> bool:
89 | ```
90 | 添加插件目录。
91 |
92 | **Args:**
93 | - `plugin_directory` (str): 要添加的插件目录路径。
94 |
95 | **Returns:**
96 | - `bool` - 添加是否成功。
97 |
98 | ### 8. 重新扫描插件目录
99 | ```python
100 | def rescan_plugin_directory() -> Tuple[int, int]:
101 | ```
102 | 重新扫描插件目录,加载新插件。
103 |
104 | **Returns:**
105 | - `Tuple[int, int]` - 成功加载的插件数量和失败的插件数量。
--------------------------------------------------------------------------------
/docs/plugins/api/tool-api.md:
--------------------------------------------------------------------------------
1 | # 工具API
2 |
3 | 工具API模块提供了获取和管理工具实例的功能,让插件能够访问系统中注册的工具。
4 |
5 | ## 导入方式
6 |
7 | ```python
8 | from src.plugin_system.apis import tool_api
9 | # 或者
10 | from src.plugin_system import tool_api
11 | ```
12 |
13 | ## 主要功能
14 |
15 | ### 1. 获取工具实例
16 |
17 | ```python
18 | def get_tool_instance(tool_name: str) -> Optional[BaseTool]:
19 | ```
20 |
21 | 获取指定名称的工具实例。
22 |
23 | **Args**:
24 | - `tool_name`: 工具名称字符串
25 |
26 | **Returns**:
27 | - `Optional[BaseTool]`: 工具实例,如果工具不存在则返回 None
28 |
29 | ### 2. 获取LLM可用的工具定义
30 |
31 | ```python
32 | def get_llm_available_tool_definitions():
33 | ```
34 |
35 | 获取所有LLM可用的工具定义列表。
36 |
37 | **Returns**:
38 | - `List[Tuple[str, Dict[str, Any]]]`: 工具定义列表,每个元素为 `(工具名称, 工具定义字典)` 的元组
39 | - 其具体定义请参照[tool-components.md](../tool-components.md#属性说明)中的工具定义格式。
40 | #### 示例:
41 |
42 | ```python
43 | # 获取所有LLM可用的工具定义
44 | tools = tool_api.get_llm_available_tool_definitions()
45 | for tool_name, tool_definition in tools:
46 | print(f"工具: {tool_name}")
47 | print(f"定义: {tool_definition}")
48 | ```
49 |
50 | ## 注意事项
51 |
52 | 1. **工具存在性检查**:使用前请检查工具实例是否为 None
53 | 2. **权限控制**:某些工具可能有使用权限限制
54 | 3. **异步调用**:大多数工具方法是异步的,需要使用 await
55 | 4. **错误处理**:调用工具时请做好异常处理
56 |
--------------------------------------------------------------------------------
/docs/plugins/index.md:
--------------------------------------------------------------------------------
1 | # MoFox_Bot插件开发文档
2 |
3 | > 欢迎来到MoFox_Bot插件系统开发文档!这里是你开始插件开发旅程的最佳起点。
4 |
5 | ## 新手入门
6 |
7 | - [📖 快速开始指南](quick-start.md) - 快速创建你的第一个插件
8 |
9 | ## 组件功能详解
10 |
11 | - [🧱 Action组件详解](action-components.md) - 掌握最核心的Action组件
12 | - [💻 Command组件详解](PLUS_COMMAND_GUIDE.md) - 学习直接响应命令的组件
13 | - [🔧 Tool组件详解](tool-components.md) - 了解如何扩展信息获取能力
14 | - [⚙️ 配置文件系统指南](configuration-guide.md) - 学会使用自动生成的插件配置文件
15 | - [📄 Manifest系统指南](manifest-guide.md) - 了解插件元数据管理和配置架构
16 |
17 | Command vs Action 选择指南
18 |
19 | 1. 使用Command的场景
20 |
21 | - ✅ 用户需要明确调用特定功能
22 | - ✅ 需要精确的参数控制
23 | - ✅ 管理和配置操作
24 | - ✅ 查询和信息显示
25 | - ✅ 系统维护命令
26 |
27 | 2. 使用Action的场景
28 |
29 | - ✅ 增强麦麦的智能行为
30 | - ✅ 根据上下文自动触发
31 | - ✅ 情绪和表情表达
32 | - ✅ 智能建议和帮助
33 | - ✅ 随机化的互动
34 |
35 |
36 | ## API浏览
37 |
38 | ### 消息发送与处理API
39 | - [📤 发送API](api/send-api.md) - 各种类型消息发送接口
40 | - [消息API](api/message-api.md) - 消息获取,消息构建,消息查询接口
41 | - [聊天流API](api/chat-api.md) - 聊天流管理和查询接口
42 |
43 | ### AI与生成API
44 | - [LLM API](api/llm-api.md) - 大语言模型交互接口,可以使用内置LLM生成内容
45 | - [✨ 回复生成器API](api/generator-api.md) - 智能回复生成接口,可以使用内置风格化生成器
46 |
47 | ### 表情包API
48 | - [😊 表情包API](api/emoji-api.md) - 表情包选择和管理接口
49 |
50 | ### 关系系统API
51 | - [人物信息API](api/person-api.md) - 用户信息,处理麦麦认识的人和关系的接口
52 |
53 | ### 数据与配置API
54 | - [🗄️ 数据库API](api/database-api.md) - 数据库操作接口
55 | - [⚙️ 配置API](api/config-api.md) - 配置读取和用户信息接口
56 |
57 | ### 插件和组件管理API
58 | - [🔌 插件API](api/plugin-manage-api.md) - 插件加载和管理接口
59 | - [🧩 组件API](api/component-manage-api.md) - 组件注册和管理接口
60 |
61 | ### 日志API
62 | - [📜 日志API](api/logging-api.md) - logger实例获取接口
63 | ### 工具API
64 | - [🔧 工具API](api/tool-api.md) - tool获取接口
65 |
66 |
67 |
68 | ## 支持
69 |
70 | > 如果你在文档中发现错误或需要补充,请:
71 |
72 | 1. 检查最新的文档版本
73 | 2. 查看相关示例代码
74 | 3. 参考其他类似插件
75 | 4. 提交文档仓库issue
76 |
77 | ## 一个方便的小设计
78 |
79 | 我们在`__init__.py`中定义了一个`__all__`变量,包含了所有需要导出的类和函数。
80 | 这样在其他地方导入时,可以直接使用 `from src.plugin_system import *` 来导入所有插件相关的类和函数。
81 | 或者你可以直接使用 `from src.plugin_system import BasePlugin, register_plugin, ComponentInfo` 之类的方式来导入你需要的部分。
--------------------------------------------------------------------------------
/plugins/bilibli/__init__.py:
--------------------------------------------------------------------------------
1 | from src.plugin_system.base.plugin_metadata import PluginMetadata
2 |
3 | __plugin_meta__ = PluginMetadata(
4 | name="Bilibili Plugin",
5 | description="A plugin for interacting with Bilibili.",
6 | usage="Usage details for Bilibili plugin.",
7 | version="1.0.0",
8 | author="Your Name",
9 | license="MIT",
10 | )
11 |
--------------------------------------------------------------------------------
/plugins/bilibli/_manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 1,
3 | "name": "哔哩哔哩视频解析插件 (Bilibili Video Parser)",
4 | "version": "1.0.0",
5 | "description": "解析哔哩哔哩视频链接,获取视频的base64编码数据(无音频版本),支持BV号、AV号和短链接",
6 | "author": {
7 | "name": "雅诺狐"
8 | },
9 | "license": "GPL-v3.0-or-later",
10 | "host_application": {
11 | "min_version": "0.8.0"
12 | },
13 | "keywords": [
14 | "bilibili",
15 | "video",
16 | "parser",
17 | "tool",
18 | "媒体处理"
19 | ],
20 | "categories": [
21 | "Media",
22 | "Tools"
23 | ],
24 | "default_locale": "zh-CN",
25 | "plugin_info": {
26 | "is_built_in": false,
27 | "plugin_type": "tool",
28 | "components": [
29 | {
30 | "type": "tool",
31 | "name": "bilibili_video_parser",
32 | "description": "解析哔哩哔哩视频链接,获取视频的base64编码数据(无音频版本)"
33 | }
34 | ],
35 | "features": [
36 | "支持BV号、AV号视频链接解析",
37 | "支持b23.tv短链接解析",
38 | "返回无音频的视频base64编码",
39 | "自动处理重定向链接",
40 | "详细的解析状态反馈"
41 | ]
42 | }
43 | }
--------------------------------------------------------------------------------
/plugins/hello_world_plugin/__init__.py:
--------------------------------------------------------------------------------
1 | from src.plugin_system.base.plugin_metadata import PluginMetadata
2 |
3 | __plugin_meta__ = PluginMetadata(
4 | name="Hello World Plugin",
5 | description="A simple hello world plugin.",
6 | usage="!hello",
7 | version="1.0.0",
8 | author="Your Name",
9 | license="MIT",
10 | )
11 |
--------------------------------------------------------------------------------
/plugins/hello_world_plugin/_manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 1,
3 | "name": "Hello World 插件",
4 | "version": "1.0.1",
5 | "description": "一个包含四大核心组件和高级配置功能的入门示例插件。",
6 | "author": {
7 | "name": "Kilo Code"
8 | },
9 | "license": "MIT",
10 | "keywords": [
11 | "example",
12 | "tutorial",
13 | "hello world"
14 | ],
15 | "categories": [
16 | "official",
17 | "example"
18 | ]
19 | }
--------------------------------------------------------------------------------
/plugins/set_emoji_like/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoFox-Studio/MoFox_Bot/3f9b3509d3c7d6fa4ea3ae40fdcb4b53b3196450/plugins/set_emoji_like/__init__.py
--------------------------------------------------------------------------------
/plugins/set_emoji_like/_manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 1,
3 | "name": "消息表情回应 (Set Message Emoji Like)",
4 | "version": "1.0.0",
5 | "description": "通过大模型判断或指令,为特定的聊天消息设置QQ表情回应。需要NapCat服务支持。",
6 | "author": {
7 | "name": "MoFox-Studio",
8 | "url": "https://github.com/MoFox-Studio"
9 | },
10 | "license": "GPL-v3.0-or-later",
11 |
12 | "host_application": {
13 | "min_version": "0.10.0"
14 | },
15 | "keywords": ["emoji", "reaction", "like", "表情", "回应", "点赞"],
16 | "categories": ["Chat", "Integration"],
17 |
18 | "default_locale": "zh-CN",
19 | "locales_path": "_locales",
20 |
21 | "plugin_info": {
22 | "is_built_in": false,
23 | "plugin_type": "functional",
24 | "components": [
25 | {
26 | "type": "action",
27 | "name": "set_emoji_like",
28 | "description": "为某条已经存在的消息添加‘贴表情’回应(类似点赞),而不是发送新消息。当用户明确要求‘贴表情’时使用。"
29 | }
30 | ],
31 | "features": [
32 | "通过LLM理解并为消息添加表情回应",
33 | "支持通过表情名称或ID指定表情"
34 | ]
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/pyrightconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/microsoft/pyright/main/packages/vscode-pyright/schemas/pyrightconfig.schema.json",
3 | "include": [
4 | "src",
5 | "bot.py"
6 | ],
7 | "exclude": [
8 | "**/__pycache__",
9 | "data",
10 | "logs",
11 | "tests",
12 | "target",
13 | "*.egg-info",
14 | "src/plugins/built_in/*",
15 | "__main__.py"
16 | ],
17 | "typeCheckingMode": "standard",
18 | "reportMissingImports": false,
19 | "reportMissingTypeStubs": false,
20 | "reportMissingModuleSource": false,
21 | "diagnosticSeverityOverrides": {
22 | "reportMissingImports": "none",
23 | "reportMissingTypeStubs": "none",
24 | "reportMissingModuleSource": "none"
25 | },
26 | "pythonVersion": "3.12",
27 | "venvPath": ".",
28 | "venv": ".venv",
29 | "executionEnvironments": [
30 | {"root": "src"}
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | sqlalchemy
2 | aiosqlite
3 | aiomysql
4 | APScheduler
5 | aiohttp
6 | aiohttp-cors
7 | colorama
8 | customtkinter
9 | dotenv
10 | faiss-cpu
11 | fastapi
12 | rjieba
13 | jsonlines
14 | maim_message
15 | quick_algo
16 | matplotlib
17 | networkx
18 | numpy
19 | openai
20 | google-genai
21 | pandas
22 | peewee
23 | pyarrow
24 | pydantic
25 | pypinyin
26 | python-dateutil
27 | python-dotenv
28 | python-igraph
29 | pymongo
30 | requests
31 | ruff
32 | scipy
33 | setuptools
34 | toml
35 | tomli
36 | tomli_w
37 | tomlkit
38 | tqdm
39 | urllib3
40 | uvicorn
41 | websockets
42 | strawberry-graphql[fastapi]
43 | httpx[socks]
44 | packaging
45 | rich
46 | psutil
47 | cryptography
48 | json-repair
49 | reportportal-client
50 | scikit-learn
51 | seaborn
52 | structlog
53 | httpx
54 | requests
55 | beautifulsoup4
56 | lxml
57 | json5
58 | toml
59 | beautifulsoup4
60 | exa_py
61 | asyncddgs
62 | opencv-python
63 | Pillow
64 | chromadb
65 | asyncio
66 | tavily-python
67 | google-generativeai
68 | lunar_python
69 | fuzzywuzzy
70 | python-multipart
71 | aiofiles
72 | inkfox
73 | mcp
74 | sse-starlette
--------------------------------------------------------------------------------
/scripts/run_lpmm.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # ==============================================
4 | # Environment Initialization
5 | # ==============================================
6 |
7 | # Step 1: Locate project root directory
8 | SCRIPTS_DIR="scripts"
9 | SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
10 | PROJECT_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
11 |
12 | # Step 2: Verify scripts directory exists
13 | if [ ! -d "$PROJECT_ROOT/$SCRIPTS_DIR" ]; then
14 | echo "❌ Error: scripts directory not found in project root" >&2
15 | echo "Current path: $PROJECT_ROOT" >&2
16 | exit 1
17 | fi
18 |
19 | # Step 3: Set up Python environment
20 | export PYTHONPATH="$PROJECT_ROOT:$PYTHONPATH"
21 | cd "$PROJECT_ROOT" || {
22 | echo "❌ Failed to cd to project root: $PROJECT_ROOT" >&2
23 | exit 1
24 | }
25 |
26 | # Debug info
27 | echo "============================"
28 | echo "Project Root: $PROJECT_ROOT"
29 | echo "Python Path: $PYTHONPATH"
30 | echo "Working Dir: $(pwd)"
31 | echo "============================"
32 |
33 | # ==============================================
34 | # Python Script Execution
35 | # ==============================================
36 |
37 | run_python_script() {
38 | local script_name=$1
39 | echo "🔄 Running $script_name"
40 | if ! python3 "$SCRIPTS_DIR/$script_name"; then
41 | echo "❌ $script_name failed" >&2
42 | exit 1
43 | fi
44 | }
45 |
46 | # Execute scripts in order
47 | run_python_script "raw_data_preprocessor.py"
48 | run_python_script "info_extraction.py"
49 | run_python_script "import_openie.py"
50 |
51 | echo "✅ All scripts completed successfully"
--------------------------------------------------------------------------------
/scripts/run_multi_stage_smoke.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """
3 | 轻量烟雾测试:初始化 MemorySystem 并运行一次检索,验证 MemoryMetadata.source 访问不再报错
4 | """
5 |
6 | import asyncio
7 | import os
8 | import sys
9 |
10 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
11 |
12 | from src.chat.memory_system.memory_system import MemorySystem
13 |
14 |
15 | async def main():
16 | ms = MemorySystem()
17 | await ms.initialize()
18 | results = await ms.retrieve_relevant_memories(query_text="测试查询:杰瑞喵喜欢什么?", limit=3)
19 | print(f"检索到 {len(results)} 条记忆(如果 >0 则表明运行成功)")
20 | for i, m in enumerate(results, 1):
21 | print(f"{i}. id={m.metadata.memory_id} source={getattr(m.metadata, 'source', None)}")
22 |
23 |
24 | if __name__ == "__main__":
25 | asyncio.run(main())
26 |
--------------------------------------------------------------------------------
/scripts/update_prompt_imports.py:
--------------------------------------------------------------------------------
1 | """
2 | 更新Prompt类导入脚本
3 | 将旧的prompt_builder.Prompt导入更新为unified_prompt.Prompt
4 | """
5 |
6 | import os
7 |
8 | # 需要更新的文件列表
9 | files_to_update = [
10 | "src/person_info/relationship_fetcher.py",
11 | "src/mood/mood_manager.py",
12 | "src/mais4u/mais4u_chat/body_emotion_action_manager.py",
13 | "src/chat/express/expression_learner.py",
14 | "src/chat/planner_actions/planner.py",
15 | "src/mais4u/mais4u_chat/s4u_prompt.py",
16 | "src/chat/message_receive/bot.py",
17 | "src/chat/replyer/default_generator.py",
18 | "src/chat/express/expression_selector.py",
19 | "src/mais4u/mai_think.py",
20 | "src/mais4u/mais4u_chat/s4u_mood_manager.py",
21 | "src/plugin_system/core/tool_use.py",
22 | "src/chat/memory_system/memory_activator.py",
23 | "src/chat/utils/smart_prompt.py",
24 | ]
25 |
26 |
27 | def update_prompt_imports(file_path):
28 | """更新文件中的Prompt导入"""
29 | if not os.path.exists(file_path):
30 | print(f"文件不存在: {file_path}")
31 | return False
32 |
33 | with open(file_path, encoding="utf-8") as f:
34 | content = f.read()
35 |
36 | # 替换导入语句
37 | old_import = "from src.chat.utils.prompt_builder import Prompt, global_prompt_manager"
38 | new_import = "from src.chat.utils.prompt import Prompt, global_prompt_manager"
39 |
40 | if old_import in content:
41 | new_content = content.replace(old_import, new_import)
42 | with open(file_path, "w", encoding="utf-8") as f:
43 | f.write(new_content)
44 | print(f"已更新: {file_path}")
45 | return True
46 | else:
47 | print(f"无需更新: {file_path}")
48 | return False
49 |
50 |
51 | def main():
52 | """主函数"""
53 | updated_count = 0
54 | for file_path in files_to_update:
55 | if update_prompt_imports(file_path):
56 | updated_count += 1
57 |
58 | print(f"\n更新完成!共更新了 {updated_count} 个文件")
59 |
60 |
61 | if __name__ == "__main__":
62 | main()
63 |
--------------------------------------------------------------------------------
/src/__init__.py:
--------------------------------------------------------------------------------
1 | import random
2 | from collections.abc import Sequence
3 | from typing import List, Optional
4 |
5 | from colorama import Fore, init
6 |
7 | from src.common.logger import get_logger
8 |
9 | egg = get_logger("小彩蛋")
10 |
11 |
12 | def weighted_choice(data: Sequence[str], weights: list[float] | None = None) -> str:
13 | """
14 | 从 data 中按权重随机返回一条。
15 | 若 weights 为 None,则所有元素权重默认为 1。
16 | """
17 | if weights is None:
18 | weights = [1.0] * len(data)
19 |
20 | if len(data) != len(weights):
21 | raise ValueError("data 和 weights 长度必须相等")
22 |
23 | # 计算累计权重区间
24 | total = 0.0
25 | acc = []
26 | for w in weights:
27 | total += w
28 | acc.append(total)
29 |
30 | if total <= 0:
31 | raise ValueError("总权重必须大于 0")
32 |
33 | # 随机落点
34 | r = random.random() * total
35 | # 二分查找落点所在的区间
36 | left, right = 0, len(acc) - 1
37 | while left < right:
38 | mid = (left + right) // 2
39 | if r < acc[mid]:
40 | right = mid
41 | else:
42 | left = mid + 1
43 | return data[left]
44 |
45 |
46 | class BaseMain:
47 | """基础主程序类"""
48 |
49 | def __init__(self):
50 | """初始化基础主程序"""
51 | self.easter_egg()
52 |
53 | @staticmethod
54 | def easter_egg():
55 | # 彩蛋
56 | init()
57 | items = [
58 | "多年以后,面对AI行刑队,张三将会回想起他2023年在会议上讨论人工智能的那个下午",
59 | "你知道吗?诺狐的耳朵很软,很好rua",
60 | "喵喵~你的麦麦被猫娘入侵了喵~",
61 | ]
62 | w = [10, 5, 2]
63 | text = weighted_choice(items, w)
64 | rainbow_colors = [Fore.RED, Fore.YELLOW, Fore.GREEN, Fore.CYAN, Fore.BLUE, Fore.MAGENTA]
65 | rainbow_text = ""
66 | for i, char in enumerate(text):
67 | rainbow_text += rainbow_colors[i % len(rainbow_colors)] + char
68 | egg.info(rainbow_text)
69 |
--------------------------------------------------------------------------------
/src/api/__init__.py:
--------------------------------------------------------------------------------
1 | # This file makes src/api a Python package.
2 |
--------------------------------------------------------------------------------
/src/chat/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | MaiBot模块系统
3 | 包含聊天、情绪、记忆、日程等功能模块
4 | """
5 |
6 | from src.chat.emoji_system.emoji_manager import get_emoji_manager
7 | from src.chat.message_receive.chat_stream import get_chat_manager
8 |
9 | # 导出主要组件供外部使用
10 | __all__ = [
11 | "get_chat_manager",
12 | "get_emoji_manager",
13 | ]
14 |
--------------------------------------------------------------------------------
/src/chat/antipromptinjector/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | MaiBot 反注入系统模块
3 |
4 | 本模块提供了一个完整的LLM反注入检测和防护系统,用于防止恶意的提示词注入攻击。
5 |
6 | 主要功能:
7 | 1. 基于规则的快速检测
8 | 2. 黑白名单机制
9 | 3. LLM二次分析
10 | 4. 消息处理模式(严格模式/宽松模式/反击模式)
11 |
12 | 作者: FOX YaNuo
13 | """
14 |
15 | from .anti_injector import AntiPromptInjector, get_anti_injector, initialize_anti_injector
16 | from .core import MessageShield, PromptInjectionDetector
17 | from .decision import CounterAttackGenerator, ProcessingDecisionMaker
18 | from .management import AntiInjectionStatistics, UserBanManager
19 | from .processors.message_processor import MessageProcessor
20 | from .types import DetectionResult, ProcessResult
21 |
22 | __all__ = [
23 | "AntiInjectionStatistics",
24 | "AntiPromptInjector",
25 | "CounterAttackGenerator",
26 | "DetectionResult",
27 | "MessageProcessor",
28 | "MessageShield",
29 | "ProcessResult",
30 | "ProcessingDecisionMaker",
31 | "PromptInjectionDetector",
32 | "UserBanManager",
33 | "get_anti_injector",
34 | "initialize_anti_injector",
35 | ]
36 |
37 |
38 | __author__ = "FOX YaNuo"
39 |
--------------------------------------------------------------------------------
/src/chat/antipromptinjector/core/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 反注入系统核心检测模块
3 |
4 | 包含:
5 | - detector: 提示词注入检测器
6 | - shield: 消息防护盾
7 | """
8 |
9 | from .detector import PromptInjectionDetector
10 | from .shield import MessageShield
11 |
12 | __all__ = ["MessageShield", "PromptInjectionDetector"]
13 |
--------------------------------------------------------------------------------
/src/chat/antipromptinjector/decision/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 反注入系统决策模块
3 |
4 | 包含:
5 | - decision_maker: 处理决策制定器
6 | - counter_attack: 反击消息生成器
7 | """
8 |
9 | from .counter_attack import CounterAttackGenerator
10 | from .decision_maker import ProcessingDecisionMaker
11 |
12 | __all__ = ["CounterAttackGenerator", "ProcessingDecisionMaker"]
13 |
--------------------------------------------------------------------------------
/src/chat/antipromptinjector/management/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 反注入系统管理模块
3 |
4 | 包含:
5 | - statistics: 统计数据管理
6 | - user_ban: 用户封禁管理
7 | """
8 |
9 | from .statistics import AntiInjectionStatistics
10 | from .user_ban import UserBanManager
11 |
12 | __all__ = ["AntiInjectionStatistics", "UserBanManager"]
13 |
--------------------------------------------------------------------------------
/src/chat/antipromptinjector/processors/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 反注入系统消息处理模块
3 |
4 | 包含:
5 | - message_processor: 消息内容处理器
6 | """
7 |
8 | from .message_processor import MessageProcessor
9 |
10 | __all__ = ["MessageProcessor"]
11 |
--------------------------------------------------------------------------------
/src/chat/antipromptinjector/types.py:
--------------------------------------------------------------------------------
1 | """
2 | 反注入系统数据类型定义模块
3 |
4 | 本模块定义了反注入系统使用的数据类型、枚举和数据结构:
5 | - ProcessResult: 处理结果枚举
6 | - DetectionResult: 检测结果数据类
7 |
8 | 实际的配置从 global_config.anti_prompt_injection 获取。
9 | """
10 |
11 | import time
12 | from dataclasses import dataclass, field
13 | from enum import Enum
14 |
15 |
16 | class ProcessResult(Enum):
17 | """处理结果枚举"""
18 |
19 | ALLOWED = "allowed" # 允许通过
20 | BLOCKED_INJECTION = "blocked_injection" # 被阻止-注入攻击
21 | BLOCKED_BAN = "blocked_ban" # 被阻止-用户封禁
22 | SHIELDED = "shielded" # 已加盾处理
23 | COUNTER_ATTACK = "counter_attack" # 反击模式-使用LLM反击并丢弃消息
24 |
25 |
26 | @dataclass
27 | class DetectionResult:
28 | """检测结果类"""
29 |
30 | is_injection: bool = False
31 | confidence: float = 0.0
32 | matched_patterns: list[str] = field(default_factory=list)
33 | llm_analysis: str | None = None
34 | processing_time: float = 0.0
35 | detection_method: str = "unknown"
36 | reason: str = ""
37 |
38 | def __post_init__(self):
39 | """结果后处理"""
40 | self.timestamp = time.time()
41 |
--------------------------------------------------------------------------------
/src/chat/emoji_system/emoji_history.py:
--------------------------------------------------------------------------------
1 | """
2 | 表情包发送历史记录模块
3 | """
4 |
5 | from collections import deque
6 |
7 | from src.common.logger import get_logger
8 |
9 | logger = get_logger("EmojiHistory")
10 |
11 | MAX_HISTORY_SIZE = 5 # 每个聊天会话最多保留最近5条表情历史
12 |
13 | # 使用一个全局字典在内存中存储历史记录
14 | # 键是 chat_id,值是一个 deque 对象
15 | _history_cache: dict[str, deque] = {}
16 |
17 |
18 | def add_emoji_to_history(chat_id: str, emoji_description: str):
19 | """
20 | 将发送的表情包添加到内存历史记录中。
21 |
22 | :param chat_id: 聊天会话ID (例如 "private_12345" 或 "group_67890")
23 | :param emoji_description: 发送的表情包的描述
24 | """
25 | if not chat_id or not emoji_description:
26 | return
27 |
28 | # 如果当前聊天还没有历史记录,则创建一个新的 deque
29 | if chat_id not in _history_cache:
30 | _history_cache[chat_id] = deque(maxlen=MAX_HISTORY_SIZE)
31 |
32 | # 添加新表情到历史记录
33 | history = _history_cache[chat_id]
34 | history.append(emoji_description)
35 |
36 | logger.debug(f"已将表情 '{emoji_description}' 添加到聊天 {chat_id} 的内存历史中")
37 |
38 |
39 | def get_recent_emojis(chat_id: str, limit: int = 5) -> list[str]:
40 | """
41 | 从内存中获取最近发送的表情包描述列表。
42 |
43 | :param chat_id: 聊天会话ID
44 | :param limit: 获取的表情数量上限
45 | :return: 最近发送的表情包描述列表
46 | """
47 | if not chat_id or chat_id not in _history_cache:
48 | return []
49 |
50 | history = _history_cache[chat_id]
51 |
52 | # 从 deque 的右侧(即最近添加的)开始取
53 | num_to_get = min(limit, len(history))
54 | recent_emojis = [history[-i] for i in range(1, num_to_get + 1)]
55 |
56 | logger.debug(f"为聊天 {chat_id} 从内存中获取到最近 {len(recent_emojis)} 个表情: {recent_emojis}")
57 | return recent_emojis
58 |
--------------------------------------------------------------------------------
/src/chat/energy_system/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 能量系统模块
3 | 提供稳定、高效的聊天流能量计算和管理功能
4 | """
5 |
6 | from .energy_manager import (
7 | ActivityEnergyCalculator,
8 | EnergyCalculator,
9 | EnergyComponent,
10 | EnergyLevel,
11 | EnergyManager,
12 | InterestEnergyCalculator,
13 | RecencyEnergyCalculator,
14 | RelationshipEnergyCalculator,
15 | energy_manager,
16 | )
17 |
18 | __all__ = [
19 | "ActivityEnergyCalculator",
20 | "EnergyCalculator",
21 | "EnergyComponent",
22 | "EnergyLevel",
23 | "EnergyManager",
24 | "InterestEnergyCalculator",
25 | "RecencyEnergyCalculator",
26 | "RelationshipEnergyCalculator",
27 | "energy_manager",
28 | ]
29 |
--------------------------------------------------------------------------------
/src/chat/frequency_analyzer/tracker.py:
--------------------------------------------------------------------------------
1 | import time
2 | from pathlib import Path
3 |
4 | import orjson
5 |
6 | from src.common.logger import get_logger
7 |
8 | # 数据存储路径
9 | DATA_DIR = Path("data/frequency_analyzer")
10 | DATA_DIR.mkdir(parents=True, exist_ok=True)
11 | TRACKER_FILE = DATA_DIR / "chat_timestamps.json"
12 |
13 | logger = get_logger("ChatFrequencyTracker")
14 |
15 |
16 | class ChatFrequencyTracker:
17 | """
18 | 负责跟踪和存储用户聊天启动时间戳。
19 | """
20 |
21 | def __init__(self):
22 | self._timestamps: dict[str, list[float]] = self._load_timestamps()
23 |
24 | @staticmethod
25 | def _load_timestamps() -> dict[str, list[float]]:
26 | """从本地文件加载时间戳数据。"""
27 | if not TRACKER_FILE.exists():
28 | return {}
29 | try:
30 | with open(TRACKER_FILE, "rb") as f:
31 | data = orjson.loads(f.read())
32 | logger.info(f"成功从 {TRACKER_FILE} 加载了聊天时间戳数据。")
33 | return data
34 | except orjson.JSONDecodeError:
35 | logger.warning(f"无法解析 {TRACKER_FILE},将创建一个新的空数据文件。")
36 | return {}
37 | except Exception as e:
38 | logger.error(f"加载聊天时间戳数据时发生未知错误: {e}")
39 | return {}
40 |
41 | def _save_timestamps(self):
42 | """将当前的时间戳数据保存到本地文件。"""
43 | try:
44 | with open(TRACKER_FILE, "wb") as f:
45 | f.write(orjson.dumps(self._timestamps))
46 | except Exception as e:
47 | logger.error(f"保存聊天时间戳数据到 {TRACKER_FILE} 时失败: {e}")
48 |
49 | def record_chat_start(self, chat_id: str):
50 | """
51 | 记录一次聊天会话的开始。
52 |
53 | Args:
54 | chat_id (str): 唯一的聊天标识符 (例如,用户ID)。
55 | """
56 | now = time.time()
57 | if chat_id not in self._timestamps:
58 | self._timestamps[chat_id] = []
59 |
60 | self._timestamps[chat_id].append(now)
61 | logger.debug(f"为 chat_id '{chat_id}' 记录了新的聊天时间: {now}")
62 | self._save_timestamps()
63 |
64 | def get_timestamps_for_chat(self, chat_id: str) -> list[float] | None:
65 | """
66 | 获取指定聊天的所有时间戳记录。
67 |
68 | Args:
69 | chat_id (str): 聊天标识符。
70 |
71 | Returns:
72 | Optional[List[float]]: 时间戳列表,如果不存在则返回 None。
73 | """
74 | return self._timestamps.get(chat_id)
75 |
76 |
77 | # 创建一个全局单例
78 | chat_frequency_tracker = ChatFrequencyTracker()
79 |
--------------------------------------------------------------------------------
/src/chat/interest_system/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 兴趣度系统模块
3 | 提供机器人兴趣标签和智能匹配功能,以及消息兴趣值计算功能
4 | """
5 |
6 | from src.common.data_models.bot_interest_data_model import BotInterestTag, BotPersonalityInterests, InterestMatchResult
7 |
8 | from .bot_interest_manager import BotInterestManager, bot_interest_manager
9 | from .interest_manager import InterestManager, get_interest_manager
10 |
11 | __all__ = [
12 | # 机器人兴趣标签管理
13 | "BotInterestManager",
14 | "BotInterestTag",
15 | "BotPersonalityInterests",
16 | # 消息兴趣值计算管理
17 | "InterestManager",
18 | "InterestMatchResult",
19 | "bot_interest_manager",
20 | "get_interest_manager",
21 | ]
22 |
--------------------------------------------------------------------------------
/src/chat/knowledge/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoFox-Studio/MoFox_Bot/3f9b3509d3c7d6fa4ea3ae40fdcb4b53b3196450/src/chat/knowledge/__init__.py
--------------------------------------------------------------------------------
/src/chat/knowledge/global_logger.py:
--------------------------------------------------------------------------------
1 | # Configure logger
2 |
3 | from src.common.logger import get_logger
4 |
5 | logger = get_logger("lpmm")
6 |
--------------------------------------------------------------------------------
/src/chat/knowledge/knowledge_lib.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from src.chat.knowledge.embedding_store import EmbeddingManager
4 | from src.chat.knowledge.global_logger import logger
5 | from src.chat.knowledge.kg_manager import KGManager
6 | from src.chat.knowledge.qa_manager import QAManager
7 | from src.config.config import global_config
8 |
9 | INVALID_ENTITY = [
10 | "",
11 | "你",
12 | "他",
13 | "她",
14 | "它",
15 | "我们",
16 | "你们",
17 | "他们",
18 | "她们",
19 | "它们",
20 | ]
21 |
22 | RAG_GRAPH_NAMESPACE = "rag-graph"
23 | RAG_ENT_CNT_NAMESPACE = "rag-ent-cnt"
24 | RAG_PG_HASH_NAMESPACE = "rag-pg-hash"
25 |
26 |
27 | ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
28 | DATA_PATH = os.path.join(ROOT_PATH, "data")
29 |
30 |
31 | qa_manager = None
32 | inspire_manager = None
33 |
34 |
35 | def initialize_lpmm_knowledge():
36 | """初始化LPMM知识库"""
37 | global qa_manager, inspire_manager
38 |
39 | # 检查LPMM知识库是否启用
40 | if global_config.lpmm_knowledge.enable:
41 | logger.info("正在初始化Mai-LPMM")
42 | logger.info("创建LLM客户端")
43 |
44 | # 初始化Embedding库
45 | embed_manager = EmbeddingManager()
46 | logger.info("正在从文件加载Embedding库")
47 | try:
48 | embed_manager.load_from_file()
49 | except Exception as e:
50 | logger.warning(f"此消息不会影响正常使用:从文件加载Embedding库时,{e}")
51 | # logger.warning("如果你是第一次导入知识,或者还未导入知识,请忽略此错误")
52 | logger.info("Embedding库加载完成")
53 | # 初始化KG
54 | kg_manager = KGManager()
55 | logger.info("正在从文件加载KG")
56 | try:
57 | kg_manager.load_from_file()
58 | except Exception as e:
59 | logger.warning(f"此消息不会影响正常使用:从文件加载KG时,{e}")
60 | # logger.warning("如果你是第一次导入知识,或者还未导入知识,请忽略此错误")
61 | logger.info("KG加载完成")
62 |
63 | logger.info(f"KG节点数量:{len(kg_manager.graph.get_node_list())}")
64 | logger.info(f"KG边数量:{len(kg_manager.graph.get_edge_list())}")
65 |
66 | # 数据比对:Embedding库与KG的段落hash集合
67 | for pg_hash in kg_manager.stored_paragraph_hashes:
68 | key = f"paragraph-{pg_hash}"
69 | if key not in embed_manager.stored_pg_hashes:
70 | logger.warning(f"KG中存在Embedding库中不存在的段落:{key}")
71 |
72 | # 问答系统(用于知识库)
73 | qa_manager = QAManager(
74 | embed_manager,
75 | kg_manager,
76 | )
77 |
78 | # # 记忆激活(用于记忆库)
79 | # inspire_manager = MemoryActiveManager(
80 | # embed_manager,
81 | # llm_client_list[global_config["embedding"]["provider"]],
82 | # )
83 | else:
84 | logger.info("LPMM知识库已禁用,跳过初始化")
85 | # 创建空的占位符对象,避免导入错误
86 |
--------------------------------------------------------------------------------
/src/chat/knowledge/prompt_template.py:
--------------------------------------------------------------------------------
1 | entity_extract_system_prompt = """你是一个性能优异的实体提取系统。请从段落中提取出所有实体,并以JSON列表的形式输出。
2 |
3 | 输出格式示例:
4 | [ "实体A", "实体B", "实体C" ]
5 |
6 | 请注意以下要求:
7 | - 将代词(如“你”、“我”、“他”、“她”、“它”等)转化为对应的实体命名,以避免指代不清。
8 | - 尽可能多的提取出段落中的全部实体;
9 | """
10 |
11 |
12 | def build_entity_extract_context(paragraph: str) -> str:
13 | """构建实体提取的完整提示文本"""
14 | return f"""{entity_extract_system_prompt}
15 |
16 | 段落:
17 | ```
18 | {paragraph}
19 | ```"""
20 |
21 |
22 | rdf_triple_extract_system_prompt = """你是一个性能优异的RDF(资源描述框架,由节点和边组成,节点表示实体/资源、属性,边则表示了实体和实体之间的关系以及实体和属性的关系。)构造系统。你的任务是根据给定的段落和实体列表构建RDF图。
23 |
24 | 请使用JSON回复,使用三元组的JSON列表输出RDF图中的关系(每个三元组代表一个关系)。
25 |
26 | 输出格式示例:
27 | [
28 | ["某实体","关系","某属性"],
29 | ["某实体","关系","某实体"],
30 | ["某资源","关系","某属性"]
31 | ]
32 |
33 | 请注意以下要求:
34 | - 每个三元组应包含每个段落的实体命名列表中的至少一个命名实体,但最好是两个。
35 | - 将代词(如“你”、“我”、“他”、“她”、“它”等)转化为对应的实体命名,以避免指代不清。
36 | """
37 |
38 |
39 | def build_rdf_triple_extract_context(paragraph: str, entities: str) -> str:
40 | """构建RDF三元组提取的完整提示文本"""
41 | return f"""{rdf_triple_extract_system_prompt}
42 |
43 | 段落:
44 | ```
45 | {paragraph}
46 | ```
47 |
48 | 实体列表:
49 | ```
50 | {entities}
51 | ```"""
52 |
53 |
54 | qa_system_prompt = """
55 | 你是一个性能优异的QA系统。请根据给定的问题和一些可能对你有帮助的信息作出回答。
56 |
57 | 请注意以下要求:
58 | - 你可以使用给定的信息来回答问题,但请不要直接引用它们。
59 | - 你的回答应该简洁明了,避免冗长的解释。
60 | - 如果你无法回答问题,请直接说“我不知道”。
61 | """
62 |
63 |
64 | # def build_qa_context(question: str, knowledge: list[tuple[str, str, str]]) -> list[LLMMessage]:
65 | # knowledge = "\n".join([f"{i + 1}. 相关性:{k[0]}\n{k[1]}" for i, k in enumerate(knowledge)])
66 | # messages = [
67 | # LLMMessage("system", qa_system_prompt).to_dict(),
68 | # LLMMessage("user", f"问题:\n{question}\n\n可能有帮助的信息:\n{knowledge}").to_dict(),
69 | # ]
70 | # return messages
71 |
--------------------------------------------------------------------------------
/src/chat/knowledge/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoFox-Studio/MoFox_Bot/3f9b3509d3c7d6fa4ea3ae40fdcb4b53b3196450/src/chat/knowledge/utils/__init__.py
--------------------------------------------------------------------------------
/src/chat/knowledge/utils/dyn_topk.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 |
4 | def dyn_select_top_k(
5 | score: list[tuple[Any, float]], jmp_factor: float, var_factor: float
6 | ) -> list[tuple[Any, float, float]]:
7 | """动态TopK选择"""
8 | # 检查输入列表是否为空
9 | if not score:
10 | return []
11 |
12 | # 按照分数排序(降序)
13 | sorted_score = sorted(score, key=lambda x: x[1], reverse=True)
14 |
15 | # 归一化
16 | max_score = sorted_score[0][1]
17 | min_score = sorted_score[-1][1]
18 | normalized_score = []
19 | for score_item in sorted_score:
20 | normalized_score.append(
21 | (
22 | score_item[0],
23 | score_item[1],
24 | (score_item[1] - min_score) / (max_score - min_score),
25 | )
26 | )
27 |
28 | # 寻找跳变点:score变化最大的位置
29 | jump_idx = 0
30 | for i in range(1, len(normalized_score)):
31 | if abs(normalized_score[i][2] - normalized_score[i - 1][2]) > abs(
32 | normalized_score[jump_idx][2] - normalized_score[jump_idx - 1][2]
33 | ):
34 | jump_idx = i
35 | # 跳变阈值
36 | jump_threshold = normalized_score[jump_idx][2]
37 |
38 | # 计算均值
39 | mean_score = sum([s[2] for s in normalized_score]) / len(normalized_score)
40 | # 计算方差
41 | var_score = sum([(s[2] - mean_score) ** 2 for s in normalized_score]) / len(normalized_score)
42 |
43 | # 动态阈值
44 | threshold = jmp_factor * jump_threshold + (1 - jmp_factor) * (mean_score + var_factor * var_score)
45 |
46 | # 重新过滤
47 | res = [s for s in normalized_score if s[2] > threshold]
48 |
49 | return res
50 |
--------------------------------------------------------------------------------
/src/chat/knowledge/utils/hash.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 |
3 |
4 | def get_sha256(string: str) -> str:
5 | """获取字符串的SHA256值"""
6 | sha256 = hashlib.sha256()
7 | sha256.update(string.encode("utf-8"))
8 | return sha256.hexdigest()
9 |
--------------------------------------------------------------------------------
/src/chat/knowledge/utils/json_fix.py:
--------------------------------------------------------------------------------
1 | import orjson
2 | from json_repair import repair_json
3 |
4 |
5 | def _find_unclosed(json_str):
6 | """
7 | Identifies the unclosed braces and brackets in the JSON string.
8 |
9 | Args:
10 | json_str (str): The JSON string to analyze.
11 |
12 | Returns:
13 | list: A list of unclosed elements in the order they were opened.
14 | """
15 | unclosed = []
16 | inside_string = False
17 | escape_next = False
18 |
19 | for char in json_str:
20 | if inside_string:
21 | if escape_next:
22 | escape_next = False
23 | elif char == "\\":
24 | escape_next = True
25 | elif char == '"':
26 | inside_string = False
27 | else:
28 | if char == '"':
29 | inside_string = True
30 | elif char in "{[":
31 | unclosed.append(char)
32 | elif char in "}]":
33 | if unclosed and ((char == "}" and unclosed[-1] == "{") or (char == "]" and unclosed[-1] == "[")):
34 | unclosed.pop()
35 |
36 | return unclosed
37 |
38 |
39 | # The following code is used to fix a broken JSON string.
40 | # From HippoRAG2 (GitHub: OSU-NLP-Group/HippoRAG)
41 | def fix_broken_generated_json(json_str: str) -> str:
42 | """
43 | Fixes a malformed JSON string by:
44 | - Removing the last comma and any trailing content.
45 | - Iterating over the JSON string once to determine and fix unclosed braces or brackets.
46 | - Ensuring braces and brackets inside string literals are not considered.
47 |
48 | If the original json_str string can be successfully loaded by orjson.loads(), will directly return it without any modification.
49 |
50 | Args:
51 | json_str (str): The malformed JSON string to be fixed.
52 |
53 | Returns:
54 | str: The corrected JSON string.
55 | """
56 |
57 | try:
58 | # Try to load the JSON to see if it is valid
59 | orjson.loads(json_str)
60 | return json_str # Return as-is if valid
61 | except orjson.JSONDecodeError:
62 | ...
63 |
64 | # Step 1: Remove trailing content after the last comma.
65 | last_comma_index = json_str.rfind(",")
66 | if last_comma_index != -1:
67 | json_str = json_str[:last_comma_index]
68 |
69 | # Step 2: Identify unclosed braces and brackets.
70 | unclosed_elements = _find_unclosed(json_str)
71 |
72 | # Step 3: Append the necessary closing elements in reverse order of opening.
73 | closing_map = {"{": "}", "[": "]"}
74 | for open_char in reversed(unclosed_elements):
75 | json_str += closing_map[open_char]
76 |
77 | return json_str
78 |
79 |
80 | def new_fix_broken_generated_json(json_str: str) -> str:
81 | """
82 | 使用 json-repair 库修复格式错误的 JSON 字符串。
83 |
84 | 如果原始 json_str 字符串可以被 orjson.loads() 成功加载,则直接返回而不进行任何修改。
85 |
86 | 参数:
87 | json_str (str): 需要修复的格式错误的 JSON 字符串。
88 |
89 | 返回:
90 | str: 修复后的 JSON 字符串。
91 | """
92 | try:
93 | # 尝试加载 JSON 以查看其是否有效
94 | orjson.loads(json_str)
95 | return json_str # 如果有效则按原样返回
96 | except orjson.JSONDecodeError:
97 | # 如果无效,则尝试修复它
98 | return repair_json(json_str)
99 |
--------------------------------------------------------------------------------
/src/chat/memory_system/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 简化记忆系统模块
3 | 移除即时记忆和长期记忆分类,实现统一记忆架构和智能遗忘机制
4 | """
5 |
6 | # 核心数据结构
7 | # 激活器
8 | from .enhanced_memory_activator import MemoryActivator, enhanced_memory_activator, memory_activator
9 | from .memory_chunk import (
10 | ConfidenceLevel,
11 | ContentStructure,
12 | ImportanceLevel,
13 | MemoryChunk,
14 | MemoryMetadata,
15 | MemoryType,
16 | create_memory_chunk,
17 | )
18 |
19 | # 兼容性别名
20 | from .memory_chunk import MemoryChunk as Memory
21 |
22 | # 遗忘引擎
23 | from .memory_forgetting_engine import ForgettingConfig, MemoryForgettingEngine, get_memory_forgetting_engine
24 | from .memory_formatter import format_memories_bracket_style
25 |
26 | # 记忆管理器
27 | from .memory_manager import MemoryManager, MemoryResult, memory_manager
28 |
29 | # 记忆核心系统
30 | from .memory_system import MemorySystem, MemorySystemConfig, get_memory_system, initialize_memory_system
31 |
32 | # Vector DB存储系统
33 | from .vector_memory_storage_v2 import VectorMemoryStorage, VectorStorageConfig, get_vector_memory_storage
34 |
35 | __all__ = [
36 | "ConfidenceLevel",
37 | "ContentStructure",
38 | "ForgettingConfig",
39 | "ImportanceLevel",
40 | "Memory", # 兼容性别名
41 | # 激活器
42 | "MemoryActivator",
43 | # 核心数据结构
44 | "MemoryChunk",
45 | # 遗忘引擎
46 | "MemoryForgettingEngine",
47 | # 记忆管理器
48 | "MemoryManager",
49 | "MemoryMetadata",
50 | "MemoryResult",
51 | # 记忆系统
52 | "MemorySystem",
53 | "MemorySystemConfig",
54 | "MemoryType",
55 | # Vector DB存储
56 | "VectorMemoryStorage",
57 | "VectorStorageConfig",
58 | "create_memory_chunk",
59 | "enhanced_memory_activator", # 兼容性别名
60 | # 格式化工具
61 | "format_memories_bracket_style",
62 | "get_memory_forgetting_engine",
63 | "get_memory_system",
64 | "get_vector_memory_storage",
65 | "initialize_memory_system",
66 | "memory_activator",
67 | "memory_manager",
68 | ]
69 |
70 | # 版本信息
71 | __version__ = "3.0.0"
72 | __author__ = "MoFox Team"
73 | __description__ = "简化记忆系统 - 统一记忆架构与智能遗忘机制"
74 |
--------------------------------------------------------------------------------
/src/chat/message_manager/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 消息管理器模块
3 | 提供统一的消息管理、上下文管理和流循环调度功能
4 | """
5 |
6 | from .context_manager import SingleStreamContextManager
7 | from .distribution_manager import StreamLoopManager, stream_loop_manager
8 | from .message_manager import MessageManager, message_manager
9 |
10 | __all__ = [
11 | "MessageManager",
12 | "SingleStreamContextManager",
13 | "StreamLoopManager",
14 | "message_manager",
15 | "stream_loop_manager",
16 | ]
17 |
--------------------------------------------------------------------------------
/src/chat/message_manager/sleep_manager/notification_sender.py:
--------------------------------------------------------------------------------
1 | from src.common.logger import get_logger
2 |
3 | # from ..hfc_context import HfcContext
4 |
5 | logger = get_logger("notification_sender")
6 |
7 |
8 | class NotificationSender:
9 | @staticmethod
10 | async def send_goodnight_notification(context): # type: ignore
11 | """发送晚安通知"""
12 | # try:
13 | # from ..proactive.events import ProactiveTriggerEvent
14 | # from ..proactive.proactive_thinker import ProactiveThinker
15 |
16 | # event = ProactiveTriggerEvent(source="sleep_manager", reason="goodnight")
17 | # proactive_thinker = ProactiveThinker(context, context.chat_instance.cycle_processor)
18 | # await proactive_thinker.think(event)
19 | # except Exception as e:
20 | # logger.error(f"发送晚安通知失败: {e}")
21 |
22 | @staticmethod
23 | async def send_insomnia_notification(context, reason: str): # type: ignore
24 | """发送失眠通知"""
25 | # try:
26 | # from ..proactive.events import ProactiveTriggerEvent
27 | # from ..proactive.proactive_thinker import ProactiveThinker
28 |
29 | # event = ProactiveTriggerEvent(source="sleep_manager", reason=reason)
30 | # proactive_thinker = ProactiveThinker(context, context.chat_instance.cycle_processor)
31 | # await proactive_thinker.think(event)
32 | # except Exception as e:
33 | # logger.error(f"发送失眠通知失败: {e}")
34 |
--------------------------------------------------------------------------------
/src/chat/message_manager/sleep_manager/sleep_state.py:
--------------------------------------------------------------------------------
1 | from datetime import date, datetime
2 | from enum import Enum, auto
3 |
4 | from src.common.logger import get_logger
5 | from src.manager.local_store_manager import local_storage
6 |
7 | logger = get_logger("sleep_state")
8 |
9 |
10 | class SleepState(Enum):
11 | """
12 | 定义了角色可能处于的几种睡眠状态。
13 | 这是一个状态机,用于管理角色的睡眠周期。
14 | """
15 |
16 | AWAKE = auto() # 清醒状态
17 | INSOMNIA = auto() # 失眠状态
18 | PREPARING_SLEEP = auto() # 准备入睡状态,一个短暂的过渡期
19 | SLEEPING = auto() # 正在睡觉状态
20 | WOKEN_UP = auto() # 被吵醒状态
21 |
22 |
23 | class SleepContext:
24 | """
25 | 睡眠上下文,负责封装和管理所有与睡眠相关的状态,并处理其持久化。
26 | """
27 |
28 | def __init__(self):
29 | """初始化睡眠上下文,并从本地存储加载初始状态。"""
30 | self.current_state: SleepState = SleepState.AWAKE
31 | self.sleep_buffer_end_time: datetime | None = None
32 | self.total_delayed_minutes_today: float = 0.0
33 | self.last_sleep_check_date: date | None = None
34 | self.re_sleep_attempt_time: datetime | None = None
35 | self.load()
36 |
37 | def save(self):
38 | """将当前的睡眠状态数据保存到本地存储。"""
39 | try:
40 | state = {
41 | "current_state": self.current_state.name,
42 | "sleep_buffer_end_time_ts": self.sleep_buffer_end_time.timestamp()
43 | if self.sleep_buffer_end_time
44 | else None,
45 | "total_delayed_minutes_today": self.total_delayed_minutes_today,
46 | "last_sleep_check_date_str": self.last_sleep_check_date.isoformat()
47 | if self.last_sleep_check_date
48 | else None,
49 | "re_sleep_attempt_time_ts": self.re_sleep_attempt_time.timestamp()
50 | if self.re_sleep_attempt_time
51 | else None,
52 | }
53 | local_storage["schedule_sleep_state"] = state
54 | logger.debug(f"已保存睡眠上下文: {state}")
55 | except Exception as e:
56 | logger.error(f"保存睡眠上下文失败: {e}")
57 |
58 | def load(self):
59 | """从本地存储加载并解析睡眠状态。"""
60 | try:
61 | state = local_storage["schedule_sleep_state"]
62 | if not (state and isinstance(state, dict)):
63 | logger.info("未找到本地睡眠上下文,使用默认值。")
64 | return
65 |
66 | state_name = state.get("current_state")
67 | if state_name and hasattr(SleepState, state_name):
68 | self.current_state = SleepState[state_name]
69 |
70 | end_time_ts = state.get("sleep_buffer_end_time_ts")
71 | if end_time_ts:
72 | self.sleep_buffer_end_time = datetime.fromtimestamp(end_time_ts)
73 |
74 | re_sleep_ts = state.get("re_sleep_attempt_time_ts")
75 | if re_sleep_ts:
76 | self.re_sleep_attempt_time = datetime.fromtimestamp(re_sleep_ts)
77 |
78 | self.total_delayed_minutes_today = state.get("total_delayed_minutes_today", 0.0)
79 |
80 | date_str = state.get("last_sleep_check_date_str")
81 | if date_str:
82 | self.last_sleep_check_date = datetime.fromisoformat(date_str).date()
83 |
84 | logger.info(f"成功从本地存储加载睡眠上下文: {state}")
85 | except Exception as e:
86 | logger.warning(f"加载睡眠上下文失败,将使用默认值: {e}")
87 |
--------------------------------------------------------------------------------
/src/chat/message_manager/sleep_manager/wakeup_context.py:
--------------------------------------------------------------------------------
1 | from src.common.logger import get_logger
2 | from src.manager.local_store_manager import local_storage
3 |
4 | logger = get_logger("wakeup_context")
5 |
6 |
7 | class WakeUpContext:
8 | """
9 | 唤醒上下文,负责封装和管理所有与唤醒相关的状态,并处理其持久化。
10 | """
11 |
12 | def __init__(self):
13 | """初始化唤醒上下文,并从本地存储加载初始状态。"""
14 | self.wakeup_value: float = 0.0
15 | self.is_angry: bool = False
16 | self.angry_start_time: float = 0.0
17 | self.sleep_pressure: float = 100.0 # 新增:睡眠压力
18 | self.load()
19 |
20 | def _get_storage_key(self) -> str:
21 | """获取本地存储键"""
22 | return "global_wakeup_manager_state"
23 |
24 | def load(self):
25 | """从本地存储加载状态"""
26 | state = local_storage[self._get_storage_key()]
27 | if state and isinstance(state, dict):
28 | self.wakeup_value = state.get("wakeup_value", 0.0)
29 | self.is_angry = state.get("is_angry", False)
30 | self.angry_start_time = state.get("angry_start_time", 0.0)
31 | self.sleep_pressure = state.get("sleep_pressure", 100.0)
32 | logger.info(f"成功从本地存储加载唤醒上下文: {state}")
33 | else:
34 | logger.info("未找到本地唤醒上下文,将使用默认值初始化。")
35 |
36 | def save(self):
37 | """将当前状态保存到本地存储"""
38 | state = {
39 | "wakeup_value": self.wakeup_value,
40 | "is_angry": self.is_angry,
41 | "angry_start_time": self.angry_start_time,
42 | "sleep_pressure": self.sleep_pressure,
43 | }
44 | local_storage[self._get_storage_key()] = state
45 | logger.debug(f"已将唤醒上下文保存到本地存储: {state}")
46 |
--------------------------------------------------------------------------------
/src/chat/message_receive/__init__.py:
--------------------------------------------------------------------------------
1 | from src.chat.emoji_system.emoji_manager import get_emoji_manager
2 | from src.chat.message_receive.chat_stream import get_chat_manager
3 | from src.chat.message_receive.storage import MessageStorage
4 |
5 | __all__ = [
6 | "MessageStorage",
7 | "get_chat_manager",
8 | "get_emoji_manager",
9 | ]
10 |
--------------------------------------------------------------------------------
/src/chat/message_receive/uni_message_sender.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import traceback
3 |
4 | from rich.traceback import install
5 |
6 | from src.chat.message_receive.message import MessageSending
7 | from src.chat.message_receive.storage import MessageStorage
8 | from src.chat.utils.utils import calculate_typing_time, truncate_message
9 | from src.common.logger import get_logger
10 | from src.common.message.api import get_global_api
11 |
12 | install(extra_lines=3)
13 |
14 | logger = get_logger("sender")
15 |
16 |
17 | async def send_message(message: MessageSending, show_log=True) -> bool:
18 | """合并后的消息发送函数,包含WS发送和日志记录"""
19 | message_preview = truncate_message(message.processed_plain_text, max_length=120)
20 |
21 | try:
22 | # 直接调用API发送消息
23 | await get_global_api().send_message(message)
24 | if show_log:
25 | logger.info(f"已将消息 '{message_preview}' 发往平台'{message.message_info.platform}'")
26 | return True
27 |
28 | except Exception as e:
29 | logger.error(f"发送消息 '{message_preview}' 发往平台'{message.message_info.platform}' 失败: {e!s}")
30 | traceback.print_exc()
31 | raise e # 重新抛出其他异常
32 |
33 |
34 | class HeartFCSender:
35 | """管理消息的注册、即时处理、发送和存储,并跟踪思考状态。"""
36 |
37 | def __init__(self):
38 | self.storage = MessageStorage()
39 |
40 | async def send_message(
41 | self, message: MessageSending, typing=False, set_reply=False, storage_message=True, show_log=True
42 | ):
43 | """
44 | 处理、发送并存储一条消息。
45 |
46 | 参数:
47 | message: MessageSending 对象,待发送的消息。
48 | typing: 是否模拟打字等待。
49 |
50 | 用法:
51 | - typing=True 时,发送前会有打字等待。
52 | """
53 | if not message.chat_stream:
54 | logger.error("消息缺少 chat_stream,无法发送")
55 | raise ValueError("消息缺少 chat_stream,无法发送")
56 | if not message.message_info or not message.message_info.message_id:
57 | logger.error("消息缺少 message_info 或 message_id,无法发送")
58 | raise ValueError("消息缺少 message_info 或 message_id,无法发送")
59 |
60 | chat_id = message.chat_stream.stream_id
61 | message_id = message.message_info.message_id
62 |
63 | try:
64 | if set_reply:
65 | message.build_reply()
66 | logger.debug(f"[{chat_id}] 选择回复引用消息: {message.processed_plain_text[:20]}...")
67 |
68 | await message.process()
69 |
70 | if typing:
71 | typing_time = calculate_typing_time(
72 | input_string=message.processed_plain_text,
73 | thinking_start_time=message.thinking_start_time,
74 | is_emoji=message.is_emoji,
75 | )
76 | await asyncio.sleep(typing_time)
77 |
78 | sent_msg = await send_message(message, show_log=show_log)
79 | if not sent_msg:
80 | return False
81 |
82 | if storage_message:
83 | await self.storage.store_message(message, message.chat_stream)
84 |
85 | return sent_msg
86 |
87 | except Exception as e:
88 | logger.error(f"[{chat_id}] 处理或存储消息 {message_id} 时出错: {e}")
89 | raise e
90 |
--------------------------------------------------------------------------------
/src/chat/replyer/replyer_manager.py:
--------------------------------------------------------------------------------
1 | from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager
2 | from src.chat.replyer.default_generator import DefaultReplyer
3 | from src.common.logger import get_logger
4 |
5 | logger = get_logger("ReplyerManager")
6 |
7 |
8 | class ReplyerManager:
9 | def __init__(self):
10 | self._repliers: dict[str, DefaultReplyer] = {}
11 |
12 | async def get_replyer(
13 | self,
14 | chat_stream: ChatStream | None = None,
15 | chat_id: str | None = None,
16 | request_type: str = "replyer",
17 | ) -> DefaultReplyer | None:
18 | """
19 | 获取或创建回复器实例。
20 |
21 | model_configs 仅在首次为某个 chat_id/stream_id 创建实例时有效。
22 | 后续调用将返回已缓存的实例,忽略 model_configs 参数。
23 | """
24 | stream_id = chat_stream.stream_id if chat_stream else chat_id
25 | if not stream_id:
26 | logger.warning("[ReplyerManager] 缺少 stream_id,无法获取回复器。")
27 | return None
28 |
29 | # 如果已有缓存实例,直接返回
30 | if stream_id in self._repliers:
31 | logger.debug(f"[ReplyerManager] 为 stream_id '{stream_id}' 返回已存在的回复器实例。")
32 | return self._repliers[stream_id]
33 |
34 | # 如果没有缓存,则创建新实例(首次初始化)
35 | logger.debug(f"[ReplyerManager] 为 stream_id '{stream_id}' 创建新的回复器实例并缓存。")
36 |
37 | target_stream = chat_stream
38 | if not target_stream:
39 | if chat_manager := get_chat_manager():
40 | target_stream = await chat_manager.get_stream(stream_id)
41 |
42 | if not target_stream:
43 | logger.warning(f"[ReplyerManager] 未找到 stream_id='{stream_id}' 的聊天流,无法创建回复器。")
44 | return None
45 |
46 | # model_configs 只在此时(初始化时)生效
47 | replyer = DefaultReplyer(
48 | chat_stream=target_stream,
49 | request_type=request_type,
50 | )
51 | self._repliers[stream_id] = replyer
52 | return replyer
53 |
54 |
55 | # 创建一个全局实例
56 | replyer_manager = ReplyerManager()
57 |
--------------------------------------------------------------------------------
/src/chat/utils/memory_mappings.py:
--------------------------------------------------------------------------------
1 | """
2 | 记忆系统相关的映射表和工具函数
3 | 提供记忆类型、置信度、重要性等的中文标签映射
4 | """
5 |
6 | # 记忆类型到中文标签的完整映射表
7 | MEMORY_TYPE_CHINESE_MAPPING = {
8 | "personal_fact": "个人事实",
9 | "preference": "偏好",
10 | "event": "事件",
11 | "opinion": "观点",
12 | "relationship": "人际关系",
13 | "emotion": "情感状态",
14 | "knowledge": "知识信息",
15 | "skill": "技能能力",
16 | "goal": "目标计划",
17 | "experience": "经验教训",
18 | "contextual": "上下文信息",
19 | "unknown": "未知",
20 | }
21 |
22 | # 置信度等级到中文标签的映射表
23 | CONFIDENCE_LEVEL_CHINESE_MAPPING = {
24 | 1: "低置信度",
25 | 2: "中等置信度",
26 | 3: "高置信度",
27 | 4: "已验证",
28 | "LOW": "低置信度",
29 | "MEDIUM": "中等置信度",
30 | "HIGH": "高置信度",
31 | "VERIFIED": "已验证",
32 | "unknown": "未知",
33 | }
34 |
35 | # 重要性等级到中文标签的映射表
36 | IMPORTANCE_LEVEL_CHINESE_MAPPING = {
37 | 1: "低重要性",
38 | 2: "一般重要性",
39 | 3: "高重要性",
40 | 4: "关键重要性",
41 | "LOW": "低重要性",
42 | "NORMAL": "一般重要性",
43 | "HIGH": "高重要性",
44 | "CRITICAL": "关键重要性",
45 | "unknown": "未知",
46 | }
47 |
48 |
49 | def get_memory_type_chinese_label(memory_type: str) -> str:
50 | """获取记忆类型的中文标签
51 |
52 | Args:
53 | memory_type: 记忆类型字符串
54 |
55 | Returns:
56 | str: 对应的中文标签,如果找不到则返回"未知"
57 | """
58 | return MEMORY_TYPE_CHINESE_MAPPING.get(memory_type, "未知")
59 |
60 |
61 | def get_confidence_level_chinese_label(level) -> str:
62 | """获取置信度等级的中文标签
63 |
64 | Args:
65 | level: 置信度等级(可以是数字、字符串或枚举实例)
66 |
67 | Returns:
68 | str: 对应的中文标签,如果找不到则返回"未知"
69 | """
70 | # 处理枚举实例
71 | if hasattr(level, "value"):
72 | level = level.value
73 |
74 | # 处理数字
75 | if isinstance(level, int):
76 | return CONFIDENCE_LEVEL_CHINESE_MAPPING.get(level, "未知")
77 |
78 | # 处理字符串
79 | if isinstance(level, str):
80 | level_upper = level.upper()
81 | return CONFIDENCE_LEVEL_CHINESE_MAPPING.get(level_upper, "未知")
82 |
83 | return "未知"
84 |
85 |
86 | def get_importance_level_chinese_label(level) -> str:
87 | """获取重要性等级的中文标签
88 |
89 | Args:
90 | level: 重要性等级(可以是数字、字符串或枚举实例)
91 |
92 | Returns:
93 | str: 对应的中文标签,如果找不到则返回"未知"
94 | """
95 | # 处理枚举实例
96 | if hasattr(level, "value"):
97 | level = level.value
98 |
99 | # 处理数字
100 | if isinstance(level, int):
101 | return IMPORTANCE_LEVEL_CHINESE_MAPPING.get(level, "未知")
102 |
103 | # 处理字符串
104 | if isinstance(level, str):
105 | level_upper = level.upper()
106 | return IMPORTANCE_LEVEL_CHINESE_MAPPING.get(level_upper, "未知")
107 |
108 | return "未知"
109 |
--------------------------------------------------------------------------------
/src/chat/utils/utils_voice.py:
--------------------------------------------------------------------------------
1 | from rich.traceback import install
2 |
3 | from src.common.logger import get_logger
4 | from src.config.config import global_config, model_config
5 | from src.llm_models.utils_model import LLMRequest
6 |
7 | install(extra_lines=3)
8 |
9 | logger = get_logger("chat_voice")
10 |
11 |
12 | async def get_voice_text(voice_base64: str) -> str:
13 | """获取音频文件转录文本"""
14 | if not global_config.voice.enable_asr:
15 | logger.warning("语音识别未启用,无法处理语音消息")
16 | return "[语音]"
17 | try:
18 | _llm = LLMRequest(model_set=model_config.model_task_config.voice, request_type="audio")
19 | text = await _llm.generate_response_for_voice(voice_base64)
20 | if text is None:
21 | logger.warning("未能生成语音文本")
22 | return "[语音(文本生成失败)]"
23 |
24 | logger.debug(f"描述是{text}")
25 |
26 | return f"[语音:{text}]"
27 | except Exception as e:
28 | logger.error(f"语音转文字失败: {e!s}")
29 | return "[语音]"
30 |
--------------------------------------------------------------------------------
/src/common/__init__.py:
--------------------------------------------------------------------------------
1 | # 这个文件可以为空,但必须存在
2 |
--------------------------------------------------------------------------------
/src/common/config_helpers.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from src.config.config import global_config, model_config
4 |
5 |
6 | def resolve_embedding_dimension(fallback: int | None = None, *, sync_global: bool = True) -> int | None:
7 | """获取当前配置的嵌入向量维度。
8 |
9 | 优先顺序:
10 | 1. 模型配置中 `model_task_config.embedding.embedding_dimension`
11 | 2. 机器人配置中 `lpmm_knowledge.embedding_dimension`
12 | 3. 调用方提供的 fallback
13 | """
14 |
15 | candidates: list[int | None] = []
16 |
17 | try:
18 | embedding_task = getattr(model_config.model_task_config, "embedding", None)
19 | if embedding_task is not None:
20 | candidates.append(getattr(embedding_task, "embedding_dimension", None))
21 | except Exception:
22 | candidates.append(None)
23 |
24 | try:
25 | candidates.append(getattr(global_config.lpmm_knowledge, "embedding_dimension", None))
26 | except Exception:
27 | candidates.append(None)
28 |
29 | candidates.append(fallback)
30 |
31 | resolved: int | None = next((int(dim) for dim in candidates if dim and int(dim) > 0), None)
32 |
33 | if resolved and sync_global:
34 | try:
35 | if getattr(global_config.lpmm_knowledge, "embedding_dimension", None) != resolved:
36 | global_config.lpmm_knowledge.embedding_dimension = resolved # type: ignore[attr-defined]
37 | except Exception:
38 | pass
39 |
40 | return resolved
41 |
--------------------------------------------------------------------------------
/src/common/data_models/__init__.py:
--------------------------------------------------------------------------------
1 | import copy
2 | from typing import Any
3 |
4 |
5 | class BaseDataModel:
6 | def deepcopy(self):
7 | return copy.deepcopy(self)
8 |
9 |
10 | def temporarily_transform_class_to_dict(obj: Any) -> Any:
11 | # sourcery skip: assign-if-exp, reintroduce-else
12 | """
13 | 将对象或容器中的 BaseDataModel 子类(类对象)或 BaseDataModel 实例
14 | 递归转换为普通 dict,不修改原对象。
15 | - 对于类对象(isinstance(value, type) 且 issubclass(..., BaseDataModel)),
16 | 读取类的 __dict__ 中非 dunder 项并递归转换。
17 | - 对于实例(isinstance(value, BaseDataModel)),读取 vars(instance) 并递归转换。
18 | """
19 |
20 | def _transform(value: Any) -> Any:
21 | # 值是类对象且为 BaseDataModel 的子类
22 | if isinstance(value, type) and issubclass(value, BaseDataModel):
23 | return {k: _transform(v) for k, v in value.__dict__.items() if not k.startswith("__") and not callable(v)}
24 |
25 | # 值是 BaseDataModel 的实例
26 | if isinstance(value, BaseDataModel):
27 | return {k: _transform(v) for k, v in vars(value).items()}
28 |
29 | # 常见容器类型,递归处理
30 | if isinstance(value, dict):
31 | return {k: _transform(v) for k, v in value.items()}
32 | if isinstance(value, list):
33 | return [_transform(v) for v in value]
34 | if isinstance(value, tuple):
35 | return tuple(_transform(v) for v in value)
36 | if isinstance(value, set):
37 | return {_transform(v) for v in value}
38 | # 基本类型,直接返回
39 | return value
40 |
41 | result = _transform(obj)
42 |
43 | def flatten(target_dict: dict):
44 | flat_dict = {}
45 | for k, v in target_dict.items():
46 | if isinstance(v, dict):
47 | # 递归扁平化子字典
48 | sub_flat = flatten(v)
49 | flat_dict.update(sub_flat)
50 | else:
51 | flat_dict[k] = v
52 | return flat_dict
53 |
54 | return flatten(result) if isinstance(result, dict) else result
55 |
--------------------------------------------------------------------------------
/src/common/data_models/info_data_model.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass, field
2 | from typing import TYPE_CHECKING, Optional
3 |
4 | from src.plugin_system.base.component_types import ChatType
5 |
6 | from . import BaseDataModel
7 |
8 | if TYPE_CHECKING:
9 | from src.plugin_system.base.component_types import ActionInfo, ChatMode
10 |
11 | from .database_data_model import DatabaseMessages
12 |
13 |
14 | @dataclass
15 | class TargetPersonInfo(BaseDataModel):
16 | platform: str = field(default_factory=str)
17 | user_id: str = field(default_factory=str)
18 | user_nickname: str = field(default_factory=str)
19 | person_id: str | None = None
20 | person_name: str | None = None
21 |
22 |
23 | @dataclass
24 | class ActionPlannerInfo(BaseDataModel):
25 | action_type: str = field(default_factory=str)
26 | reasoning: str | None = None
27 | action_data: dict | None = None
28 | action_message: Optional["DatabaseMessages"] = None
29 | available_actions: dict[str, "ActionInfo"] | None = None
30 |
31 |
32 | @dataclass
33 | class InterestScore(BaseDataModel):
34 | """兴趣度评分结果"""
35 |
36 | message_id: str
37 | total_score: float
38 | interest_match_score: float
39 | relationship_score: float
40 | mentioned_score: float
41 | details: dict[str, str]
42 |
43 |
44 | @dataclass
45 | class Plan(BaseDataModel):
46 | """
47 | 统一规划数据模型
48 | """
49 |
50 | chat_id: str
51 | mode: "ChatMode"
52 |
53 | chat_type: "ChatType"
54 | # Generator 填充
55 | available_actions: dict[str, "ActionInfo"] = field(default_factory=dict)
56 | chat_history: list["DatabaseMessages"] = field(default_factory=list)
57 | target_info: TargetPersonInfo | None = None
58 |
59 | # Filter 填充
60 | llm_prompt: str | None = None
61 | decided_actions: list[ActionPlannerInfo] | None = None
62 |
--------------------------------------------------------------------------------
/src/common/data_models/llm_data_model.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from typing import TYPE_CHECKING, Any
3 |
4 | from src.llm_models.payload_content.tool_option import ToolCall
5 |
6 | from . import BaseDataModel
7 |
8 | if TYPE_CHECKING:
9 | pass
10 |
11 |
12 | @dataclass
13 | class LLMGenerationDataModel(BaseDataModel):
14 | content: str | None = None
15 | reasoning: str | None = None
16 | model: str | None = None
17 | tool_calls: list["ToolCall"] | None = None
18 | prompt: str | None = None
19 | selected_expressions: list[int] | None = None
20 | reply_set: list[tuple[str, Any]] | None = None
21 |
--------------------------------------------------------------------------------
/src/common/database/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoFox-Studio/MoFox_Bot/3f9b3509d3c7d6fa4ea3ae40fdcb4b53b3196450/src/common/database/__init__.py
--------------------------------------------------------------------------------
/src/common/database/sqlalchemy_init.py:
--------------------------------------------------------------------------------
1 | """SQLAlchemy数据库初始化模块
2 |
3 | 替换Peewee的数据库初始化逻辑
4 | 提供统一的异步数据库初始化接口
5 | """
6 |
7 | from sqlalchemy.exc import SQLAlchemyError
8 |
9 | from src.common.database.sqlalchemy_models import Base, get_engine, initialize_database
10 | from src.common.logger import get_logger
11 |
12 | logger = get_logger("sqlalchemy_init")
13 |
14 |
15 | async def initialize_sqlalchemy_database() -> bool:
16 | """
17 | 初始化SQLAlchemy异步数据库
18 | 创建所有表结构
19 |
20 | Returns:
21 | bool: 初始化是否成功
22 | """
23 | try:
24 | logger.info("开始初始化SQLAlchemy异步数据库...")
25 |
26 | # 初始化数据库引擎和会话
27 | engine, session_local = await initialize_database()
28 |
29 | if engine is None:
30 | logger.error("数据库引擎初始化失败")
31 | return False
32 |
33 | logger.info("SQLAlchemy异步数据库初始化成功")
34 | return True
35 |
36 | except SQLAlchemyError as e:
37 | logger.error(f"SQLAlchemy数据库初始化失败: {e}")
38 | return False
39 | except Exception as e:
40 | logger.error(f"数据库初始化过程中发生未知错误: {e}")
41 | return False
42 |
43 |
44 | async def create_all_tables() -> bool:
45 | """
46 | 异步创建所有数据库表
47 |
48 | Returns:
49 | bool: 创建是否成功
50 | """
51 | try:
52 | logger.info("开始创建数据库表...")
53 |
54 | engine = await get_engine()
55 | if engine is None:
56 | logger.error("无法获取数据库引擎")
57 | return False
58 |
59 | # 异步创建所有表
60 | async with engine.begin() as conn:
61 | await conn.run_sync(Base.metadata.create_all)
62 |
63 | logger.info("数据库表创建成功")
64 | return True
65 |
66 | except SQLAlchemyError as e:
67 | logger.error(f"创建数据库表失败: {e}")
68 | return False
69 | except Exception as e:
70 | logger.error(f"创建数据库表过程中发生未知错误: {e}")
71 | return False
72 |
73 |
74 | async def get_database_info() -> dict | None:
75 | """
76 | 异步获取数据库信息
77 |
78 | Returns:
79 | dict: 数据库信息字典,包含引擎信息等
80 | """
81 | try:
82 | engine = await get_engine()
83 | if engine is None:
84 | return None
85 |
86 | info = {
87 | "engine_name": engine.name,
88 | "driver": engine.driver,
89 | "url": str(engine.url).replace(engine.url.password or "", "***"), # 隐藏密码
90 | "pool_size": getattr(engine.pool, "size", None),
91 | "max_overflow": getattr(engine.pool, "max_overflow", None),
92 | }
93 |
94 | return info
95 |
96 | except Exception as e:
97 | logger.error(f"获取数据库信息失败: {e}")
98 | return None
99 |
100 |
101 | _database_initialized = False
102 |
103 |
104 | async def initialize_database_compat() -> bool:
105 | """
106 | 兼容性异步数据库初始化函数
107 | 用于替换原有的Peewee初始化代码
108 |
109 | Returns:
110 | bool: 初始化是否成功
111 | """
112 | global _database_initialized
113 |
114 | if _database_initialized:
115 | return True
116 |
117 | success = await initialize_sqlalchemy_database()
118 | if success:
119 | success = await create_all_tables()
120 |
121 | if success:
122 | _database_initialized = True
123 |
124 | return success
125 |
--------------------------------------------------------------------------------
/src/common/message/__init__.py:
--------------------------------------------------------------------------------
1 | """Maim Message - A message handling library"""
2 |
3 | __version__ = "0.1.0"
4 |
5 | from .api import get_global_api
6 |
7 | __all__ = [
8 | "get_global_api",
9 | ]
10 |
--------------------------------------------------------------------------------
/src/common/message/api.py:
--------------------------------------------------------------------------------
1 | import importlib.metadata
2 | import os
3 |
4 | from maim_message import MessageServer
5 |
6 | from src.common.logger import get_logger
7 | from src.common.server import get_global_server
8 | from src.config.config import global_config
9 |
10 | global_api = None
11 |
12 |
13 | def get_global_api() -> MessageServer: # sourcery skip: extract-method
14 | """获取全局MessageServer实例"""
15 | global global_api
16 | if global_api is None:
17 | # 检查maim_message版本
18 | try:
19 | maim_message_version = importlib.metadata.version("maim_message")
20 | version_compatible = [int(x) for x in maim_message_version.split(".")] >= [0, 3, 3]
21 | except (importlib.metadata.PackageNotFoundError, ValueError):
22 | version_compatible = False
23 |
24 | # 读取配置项
25 | maim_message_config = global_config.maim_message
26 |
27 | # 设置基本参数
28 |
29 | host = os.getenv("HOST", "127.0.0.1")
30 | port_str = os.getenv("PORT", "8000")
31 |
32 | try:
33 | port = int(port_str)
34 | except ValueError:
35 | port = 8000
36 |
37 | kwargs = {
38 | "host": host,
39 | "port": port,
40 | "app": get_global_server().get_app(),
41 | }
42 |
43 | # 只有在版本 >= 0.3.0 时才使用高级特性
44 | if version_compatible:
45 | # 添加自定义logger
46 | maim_message_logger = get_logger("maim_message")
47 | kwargs["custom_logger"] = maim_message_logger
48 |
49 | # 添加token认证
50 | if maim_message_config.auth_token and len(maim_message_config.auth_token) > 0:
51 | kwargs["enable_token"] = True
52 |
53 | if maim_message_config.use_custom:
54 | # 添加WSS模式支持
55 | del kwargs["app"]
56 | kwargs["host"] = maim_message_config.host
57 | kwargs["port"] = maim_message_config.port
58 | kwargs["mode"] = maim_message_config.mode
59 | if maim_message_config.use_wss:
60 | if maim_message_config.cert_file:
61 | kwargs["ssl_certfile"] = maim_message_config.cert_file
62 | if maim_message_config.key_file:
63 | kwargs["ssl_keyfile"] = maim_message_config.key_file
64 | kwargs["enable_custom_uvicorn_logger"] = False
65 |
66 | global_api = MessageServer(**kwargs)
67 | if version_compatible and maim_message_config.auth_token:
68 | for token in maim_message_config.auth_token:
69 | global_api.add_valid_token(token)
70 | return global_api
71 |
--------------------------------------------------------------------------------
/src/common/tcp_connector.py:
--------------------------------------------------------------------------------
1 | import ssl
2 |
3 | import aiohttp
4 | import certifi
5 |
6 | ssl_context = ssl.create_default_context(cafile=certifi.where())
7 |
8 |
9 | async def get_tcp_connector():
10 | return aiohttp.TCPConnector(ssl=ssl_context)
11 |
--------------------------------------------------------------------------------
/src/common/vector_db/__init__.py:
--------------------------------------------------------------------------------
1 | from .base import VectorDBBase
2 | from .chromadb_impl import ChromaDBImpl
3 |
4 |
5 | def get_vector_db_service() -> VectorDBBase:
6 | """
7 | 工厂函数,初始化并返回向量数据库服务实例。
8 |
9 | 目前硬编码为 ChromaDB,未来可以从配置中读取。
10 | """
11 | # TODO: 从全局配置中读取数据库类型和路径
12 | db_path = "data/chroma_db"
13 |
14 | # ChromaDBImpl 是一个单例,所以这里每次调用都会返回同一个实例
15 | return ChromaDBImpl(path=db_path)
16 |
17 |
18 | # 全局向量数据库服务实例
19 | vector_db_service: VectorDBBase = get_vector_db_service()
20 |
21 | __all__ = ["VectorDBBase", "vector_db_service"]
22 |
--------------------------------------------------------------------------------
/src/individuality/not_using/scene.py:
--------------------------------------------------------------------------------
1 | import os
2 | from typing import Any
3 |
4 | import orjson
5 |
6 |
7 | def load_scenes() -> dict[str, Any]:
8 | """
9 | 从JSON文件加载场景数据
10 |
11 | Returns:
12 | Dict: 包含所有场景的字典
13 | """
14 | current_dir = os.path.dirname(os.path.abspath(__file__))
15 | json_path = os.path.join(current_dir, "template_scene.json")
16 |
17 | with open(json_path, encoding="utf-8") as f:
18 | return orjson.loads(f.read())
19 |
20 |
21 | PERSONALITY_SCENES = load_scenes()
22 |
23 |
24 | def get_scene_by_factor(factor: str) -> dict | None:
25 | """
26 | 根据人格因子获取对应的情景测试
27 |
28 | Args:
29 | factor (str): 人格因子名称
30 |
31 | Returns:
32 | dict: 包含情景描述的字典
33 | """
34 | return PERSONALITY_SCENES.get(factor, None)
35 |
36 |
37 | def get_all_scenes() -> dict:
38 | """
39 | 获取所有情景测试
40 |
41 | Returns:
42 | Dict: 所有情景测试的字典
43 | """
44 | return PERSONALITY_SCENES
45 |
--------------------------------------------------------------------------------
/src/llm_models/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Mai.To.The.Gate
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/llm_models/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoFox-Studio/MoFox_Bot/3f9b3509d3c7d6fa4ea3ae40fdcb4b53b3196450/src/llm_models/__init__.py
--------------------------------------------------------------------------------
/src/llm_models/exceptions.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | # 常见Error Code Mapping (以OpenAI API为例)
4 | error_code_mapping = {
5 | 400: "参数不正确",
6 | 401: "API-Key错误,认证失败,请检查/config/model_list.toml中的配置是否正确",
7 | 402: "账号余额不足",
8 | 403: "模型拒绝访问,可能需要实名或余额不足",
9 | 404: "Not Found",
10 | 413: "请求体过大,请尝试压缩图片或减少输入内容",
11 | 429: "请求过于频繁,请稍后再试",
12 | 500: "服务器内部故障",
13 | 503: "服务器负载过高",
14 | }
15 |
16 |
17 | class NetworkConnectionError(Exception):
18 | """连接异常,常见于网络问题或服务器不可用"""
19 |
20 | def __init__(self):
21 | super().__init__()
22 |
23 | def __str__(self):
24 | return "连接异常,请检查网络连接状态或URL是否正确"
25 |
26 |
27 | class ReqAbortException(Exception):
28 | """请求异常退出,常见于请求被中断或取消"""
29 |
30 | def __init__(self, message: str | None = None):
31 | super().__init__(message)
32 | self.message = message
33 |
34 | def __str__(self):
35 | return self.message or "请求因未知原因异常终止"
36 |
37 |
38 | class RespNotOkException(Exception):
39 | """请求响应异常,见于请求未能成功响应(非 '200 OK')"""
40 |
41 | def __init__(self, status_code: int, message: str | None = None):
42 | super().__init__(message)
43 | self.status_code = status_code
44 | self.message = message
45 |
46 | def __str__(self):
47 | if self.status_code in error_code_mapping:
48 | return error_code_mapping[self.status_code]
49 | elif self.message:
50 | return self.message
51 | else:
52 | return f"未知的异常响应代码:{self.status_code}"
53 |
54 |
55 | class RespParseException(Exception):
56 | """响应解析错误,常见于响应格式不正确或解析方法不匹配"""
57 |
58 | def __init__(self, ext_info: Any, message: str | None = None):
59 | super().__init__(message)
60 | self.ext_info = ext_info
61 | self.message = message
62 |
63 | def __str__(self):
64 | return self.message or "解析响应内容时发生未知错误,请检查是否配置了正确的解析方法"
65 |
66 |
67 | class PayLoadTooLargeError(Exception):
68 | """自定义异常类,用于处理请求体过大错误"""
69 |
70 | def __init__(self, message: str):
71 | super().__init__(message)
72 | self.message = message
73 |
74 | def __str__(self):
75 | return "请求体过大,请尝试压缩图片或减少输入内容。"
76 |
77 |
78 | class RequestAbortException(Exception):
79 | """自定义异常类,用于处理请求中断异常"""
80 |
81 | def __init__(self, message: str):
82 | super().__init__(message)
83 | self.message = message
84 |
85 | def __str__(self):
86 | return self.message
87 |
88 |
89 | class PermissionDeniedException(Exception):
90 | """自定义异常类,用于处理访问拒绝的异常"""
91 |
92 | def __init__(self, message: str):
93 | super().__init__(message)
94 | self.message = message
95 |
96 | def __str__(self):
97 | return self.message
98 |
--------------------------------------------------------------------------------
/src/llm_models/model_client/__init__.py:
--------------------------------------------------------------------------------
1 | from src.config.config import model_config
2 |
3 | used_client_types = {provider.client_type for provider in model_config.api_providers}
4 |
5 | if "openai" in used_client_types:
6 | from . import openai_client # noqa: F401
7 | if "aiohttp_gemini" in used_client_types:
8 | from . import aiohttp_gemini_client # noqa: F401
9 | if "mcp_sse" in used_client_types:
10 | from . import mcp_sse_client # noqa: F401
11 |
--------------------------------------------------------------------------------
/src/llm_models/payload_content/__init__.py:
--------------------------------------------------------------------------------
1 | from .tool_option import ToolCall
2 |
3 | __all__ = ["ToolCall"]
4 |
--------------------------------------------------------------------------------
/src/mais4u/config/old/s4u_config_20250715_141713.toml:
--------------------------------------------------------------------------------
1 | [inner]
2 | version = "1.0.0"
3 |
4 | #----以下是S4U聊天系统配置文件----
5 | # S4U (Smart 4 U) 聊天系统是MaiBot的核心对话模块
6 | # 支持优先级队列、消息中断、VIP用户等高级功能
7 | #
8 | # 如果你想要修改配置文件,请在修改后将version的值进行变更
9 | # 如果新增项目,请参考src/mais4u/s4u_config.py中的S4UConfig类
10 | #
11 | # 版本格式:主版本号.次版本号.修订号
12 | #----S4U配置说明结束----
13 |
14 | [s4u]
15 | # 消息管理配置
16 | message_timeout_seconds = 120 # 普通消息存活时间(秒),超过此时间的消息将被丢弃
17 | recent_message_keep_count = 6 # 保留最近N条消息,超出范围的普通消息将被移除
18 |
19 | # 优先级系统配置
20 | at_bot_priority_bonus = 100.0 # @机器人时的优先级加成分数
21 | vip_queue_priority = true # 是否启用VIP队列优先级系统
22 | enable_message_interruption = true # 是否允许高优先级消息中断当前回复
23 |
24 | # 打字效果配置
25 | typing_delay = 0.1 # 打字延迟时间(秒),模拟真实打字速度
26 | enable_dynamic_typing_delay = false # 是否启用基于文本长度的动态打字延迟
27 |
28 | # 动态打字延迟参数(仅在enable_dynamic_typing_delay=true时生效)
29 | chars_per_second = 15.0 # 每秒字符数,用于计算动态打字延迟
30 | min_typing_delay = 0.2 # 最小打字延迟(秒)
31 | max_typing_delay = 2.0 # 最大打字延迟(秒)
32 |
33 | # 系统功能开关
34 | enable_old_message_cleanup = true # 是否自动清理过旧的普通消息
35 | enable_loading_indicator = true # 是否显示加载提示
36 |
37 |
--------------------------------------------------------------------------------
/src/mais4u/config/s4u_config.toml:
--------------------------------------------------------------------------------
1 | [inner]
2 | version = "1.1.0"
3 |
4 | #----以下是S4U聊天系统配置文件----
5 | # S4U (Smart 4 U) 聊天系统是MaiBot的核心对话模块
6 | # 支持优先级队列、消息中断、VIP用户等高级功能
7 | #
8 | # 如果你想要修改配置文件,请在修改后将version的值进行变更
9 | # 如果新增项目,请参考src/mais4u/s4u_config.py中的S4UConfig类
10 | #
11 | # 版本格式:主版本号.次版本号.修订号
12 | #----S4U配置说明结束----
13 |
14 | [s4u]
15 | # 消息管理配置
16 | message_timeout_seconds = 80 # 普通消息存活时间(秒),超过此时间的消息将被丢弃
17 | recent_message_keep_count = 8 # 保留最近N条消息,超出范围的普通消息将被移除
18 |
19 | # 优先级系统配置
20 | at_bot_priority_bonus = 100.0 # @机器人时的优先级加成分数
21 | vip_queue_priority = true # 是否启用VIP队列优先级系统
22 | enable_message_interruption = true # 是否允许高优先级消息中断当前回复
23 |
24 | # 打字效果配置
25 | typing_delay = 0.1 # 打字延迟时间(秒),模拟真实打字速度
26 | enable_dynamic_typing_delay = false # 是否启用基于文本长度的动态打字延迟
27 |
28 | # 动态打字延迟参数(仅在enable_dynamic_typing_delay=true时生效)
29 | chars_per_second = 15.0 # 每秒字符数,用于计算动态打字延迟
30 | min_typing_delay = 0.2 # 最小打字延迟(秒)
31 | max_typing_delay = 2.0 # 最大打字延迟(秒)
32 |
33 | # 系统功能开关
34 | enable_old_message_cleanup = true # 是否自动清理过旧的普通消息
35 | enable_loading_indicator = true # 是否显示加载提示
36 |
37 | enable_streaming_output = false # 是否启用流式输出,false时全部生成后一次性发送
38 |
39 | max_context_message_length = 30
40 | max_core_message_length = 20
41 |
42 | # 模型配置
43 | [models]
44 | # 主要对话模型配置
45 | [models.chat]
46 | name = "qwen3-8b"
47 | provider = "BAILIAN"
48 | pri_in = 0.5
49 | pri_out = 2
50 | temp = 0.7
51 | enable_thinking = false
52 |
53 | # 规划模型配置
54 | [models.motion]
55 | name = "qwen3-8b"
56 | provider = "BAILIAN"
57 | pri_in = 0.5
58 | pri_out = 2
59 | temp = 0.7
60 | enable_thinking = false
61 |
62 | # 情感分析模型配置
63 | [models.emotion]
64 | name = "qwen3-8b"
65 | provider = "BAILIAN"
66 | pri_in = 0.5
67 | pri_out = 2
68 | temp = 0.7
69 |
70 | # 记忆模型配置
71 | [models.memory]
72 | name = "qwen3-8b"
73 | provider = "BAILIAN"
74 | pri_in = 0.5
75 | pri_out = 2
76 | temp = 0.7
77 |
78 | # 工具使用模型配置
79 | [models.tool_use]
80 | name = "qwen3-8b"
81 | provider = "BAILIAN"
82 | pri_in = 0.5
83 | pri_out = 2
84 | temp = 0.7
85 |
86 | # 嵌入模型配置
87 | [models.embedding]
88 | name = "text-embedding-v1"
89 | provider = "OPENAI"
90 | dimension = 1024
91 |
92 | # 视觉语言模型配置
93 | [models.vlm]
94 | name = "qwen-vl-plus"
95 | provider = "BAILIAN"
96 | pri_in = 0.5
97 | pri_out = 2
98 | temp = 0.7
99 |
100 | # 知识库模型配置
101 | [models.knowledge]
102 | name = "qwen3-8b"
103 | provider = "BAILIAN"
104 | pri_in = 0.5
105 | pri_out = 2
106 | temp = 0.7
107 |
108 | # 实体提取模型配置
109 | [models.entity_extract]
110 | name = "qwen3-8b"
111 | provider = "BAILIAN"
112 | pri_in = 0.5
113 | pri_out = 2
114 | temp = 0.7
115 |
116 | # 问答模型配置
117 | [models.qa]
118 | name = "qwen3-8b"
119 | provider = "BAILIAN"
120 | pri_in = 0.5
121 | pri_out = 2
122 | temp = 0.7
123 |
124 | # 兼容性配置(已废弃,请使用models.motion)
125 | [model_motion] # 在麦麦的一些组件中使用的小模型,消耗量较大,建议使用速度较快的小模型
126 | # 强烈建议使用免费的小模型
127 | name = "qwen3-8b"
128 | provider = "BAILIAN"
129 | pri_in = 0.5
130 | pri_out = 2
131 | temp = 0.7
132 | enable_thinking = false # 是否启用思考
--------------------------------------------------------------------------------
/src/mais4u/config/s4u_config_template.toml:
--------------------------------------------------------------------------------
1 | [inner]
2 | version = "1.1.0"
3 |
4 | #----以下是S4U聊天系统配置文件----
5 | # S4U (Smart 4 U) 聊天系统是MaiBot的核心对话模块
6 | # 支持优先级队列、消息中断、VIP用户等高级功能
7 | #
8 | # 如果你想要修改配置文件,请在修改后将version的值进行变更
9 | # 如果新增项目,请参考src/mais4u/s4u_config.py中的S4UConfig类
10 | #
11 | # 版本格式:主版本号.次版本号.修订号
12 | #----S4U配置说明结束----
13 |
14 | [s4u]
15 | # 消息管理配置
16 | message_timeout_seconds = 120 # 普通消息存活时间(秒),超过此时间的消息将被丢弃
17 | recent_message_keep_count = 6 # 保留最近N条消息,超出范围的普通消息将被移除
18 |
19 | # 优先级系统配置
20 | at_bot_priority_bonus = 100.0 # @机器人时的优先级加成分数
21 | vip_queue_priority = true # 是否启用VIP队列优先级系统
22 | enable_message_interruption = true # 是否允许高优先级消息中断当前回复
23 |
24 | # 打字效果配置
25 | typing_delay = 0.1 # 打字延迟时间(秒),模拟真实打字速度
26 | enable_dynamic_typing_delay = false # 是否启用基于文本长度的动态打字延迟
27 |
28 | # 动态打字延迟参数(仅在enable_dynamic_typing_delay=true时生效)
29 | chars_per_second = 15.0 # 每秒字符数,用于计算动态打字延迟
30 | min_typing_delay = 0.2 # 最小打字延迟(秒)
31 | max_typing_delay = 2.0 # 最大打字延迟(秒)
32 |
33 | # 系统功能开关
34 | enable_old_message_cleanup = true # 是否自动清理过旧的普通消息
35 |
36 | enable_streaming_output = true # 是否启用流式输出,false时全部生成后一次性发送
37 |
38 | max_context_message_length = 20
39 | max_core_message_length = 30
40 |
41 | # 模型配置
42 | [models]
43 | # 主要对话模型配置
44 | [models.chat]
45 | name = "qwen3-8b"
46 | provider = "BAILIAN"
47 | pri_in = 0.5
48 | pri_out = 2
49 | temp = 0.7
50 | enable_thinking = false
51 |
52 | # 规划模型配置
53 | [models.motion]
54 | name = "qwen3-32b"
55 | provider = "BAILIAN"
56 | pri_in = 0.5
57 | pri_out = 2
58 | temp = 0.7
59 | enable_thinking = false
60 |
61 | # 情感分析模型配置
62 | [models.emotion]
63 | name = "qwen3-8b"
64 | provider = "BAILIAN"
65 | pri_in = 0.5
66 | pri_out = 2
67 | temp = 0.7
68 |
--------------------------------------------------------------------------------
/src/mais4u/constant_s4u.py:
--------------------------------------------------------------------------------
1 | ENABLE_S4U = False
2 |
--------------------------------------------------------------------------------
/src/mais4u/mais4u_chat/internal_manager.py:
--------------------------------------------------------------------------------
1 | class InternalManager:
2 | def __init__(self):
3 | self.now_internal_state = ""
4 |
5 | def set_internal_state(self, internal_state: str):
6 | self.now_internal_state = internal_state
7 |
8 | def get_internal_state(self):
9 | return self.now_internal_state
10 |
11 | def get_internal_state_str(self):
12 | return f"你今天的直播内容是直播QQ水群,你正在一边回复弹幕,一边在QQ群聊天,你在QQ群聊天中产生的想法是:{self.now_internal_state}"
13 |
14 |
15 | internal_manager = InternalManager()
16 |
--------------------------------------------------------------------------------
/src/mais4u/mais4u_chat/s4u_watching_manager.py:
--------------------------------------------------------------------------------
1 | from src.common.logger import get_logger
2 | from src.plugin_system.apis import send_api
3 |
4 | """
5 | 视线管理系统使用说明:
6 |
7 | 1. 视线状态:
8 | - wandering: 随意看
9 | - danmu: 看弹幕
10 | - lens: 看镜头
11 |
12 | 2. 状态切换逻辑:
13 | - 收到消息时 → 切换为看弹幕,立即发送更新
14 | - 开始生成回复时 → 切换为看镜头或随意,立即发送更新
15 | - 生成完毕后 → 看弹幕1秒,然后回到看镜头直到有新消息,状态变化时立即发送更新
16 |
17 | 3. 使用方法:
18 | # 获取视线管理器
19 | watching = watching_manager.get_watching_by_chat_id(chat_id)
20 |
21 | # 收到消息时调用
22 | await watching.on_message_received()
23 |
24 | # 开始生成回复时调用
25 | await watching.on_reply_start()
26 |
27 | # 生成回复完毕时调用
28 | await watching.on_reply_finished()
29 |
30 | 4. 自动更新系统:
31 | - 状态变化时立即发送type为"watching",data为状态值的websocket消息
32 | - 使用定时器自动处理状态转换(如看弹幕时间结束后自动切换到看镜头)
33 | - 无需定期检查,所有状态变化都是事件驱动的
34 | """
35 |
36 | logger = get_logger("watching")
37 |
38 | HEAD_CODE = {
39 | "看向上方": "(0,0.5,0)",
40 | "看向下方": "(0,-0.5,0)",
41 | "看向左边": "(-1,0,0)",
42 | "看向右边": "(1,0,0)",
43 | "随意朝向": "random",
44 | "看向摄像机": "camera",
45 | "注视对方": "(0,0,0)",
46 | "看向正前方": "(0,0,0)",
47 | }
48 |
49 |
50 | class ChatWatching:
51 | def __init__(self, chat_id: str):
52 | self.chat_id: str = chat_id
53 |
54 | async def on_reply_start(self):
55 | """开始生成回复时调用"""
56 | await send_api.custom_to_stream(
57 | message_type="state", content="start_thinking", stream_id=self.chat_id, storage_message=False
58 | )
59 |
60 | async def on_reply_finished(self):
61 | """生成回复完毕时调用"""
62 | await send_api.custom_to_stream(
63 | message_type="state", content="finish_reply", stream_id=self.chat_id, storage_message=False
64 | )
65 |
66 | async def on_thinking_finished(self):
67 | """思考完毕时调用"""
68 | await send_api.custom_to_stream(
69 | message_type="state", content="finish_thinking", stream_id=self.chat_id, storage_message=False
70 | )
71 |
72 | async def on_message_received(self):
73 | """收到消息时调用"""
74 | await send_api.custom_to_stream(
75 | message_type="state", content="start_viewing", stream_id=self.chat_id, storage_message=False
76 | )
77 |
78 | async def on_internal_message_start(self):
79 | """收到消息时调用"""
80 | await send_api.custom_to_stream(
81 | message_type="state", content="start_internal_thinking", stream_id=self.chat_id, storage_message=False
82 | )
83 |
84 |
85 | class WatchingManager:
86 | def __init__(self):
87 | self.watching_list: list[ChatWatching] = []
88 | """当前视线状态列表"""
89 | self.task_started: bool = False
90 |
91 | def get_watching_by_chat_id(self, chat_id: str) -> ChatWatching:
92 | """获取或创建聊天对应的视线管理器"""
93 | for watching in self.watching_list:
94 | if watching.chat_id == chat_id:
95 | return watching
96 |
97 | new_watching = ChatWatching(chat_id)
98 | self.watching_list.append(new_watching)
99 | logger.info(f"为chat {chat_id}创建新的视线管理器")
100 |
101 | return new_watching
102 |
103 |
104 | # 全局视线管理器实例
105 | watching_manager = WatchingManager()
106 | """全局视线管理器"""
107 |
--------------------------------------------------------------------------------
/src/mais4u/mais4u_chat/screen_manager.py:
--------------------------------------------------------------------------------
1 | class ScreenManager:
2 | def __init__(self):
3 | self.now_screen = ""
4 |
5 | def set_screen(self, screen_str: str):
6 | self.now_screen = screen_str
7 |
8 | def get_screen(self):
9 | return self.now_screen
10 |
11 | def get_screen_str(self):
12 | return f"你可以看见面前的屏幕,目前屏幕的内容是:现在千石可乐在和你一起直播,这是他正在操作的屏幕内容:{self.now_screen}"
13 |
14 |
15 | screen_manager = ScreenManager()
16 |
--------------------------------------------------------------------------------
/src/mais4u/mais4u_chat/yes_or_no.py:
--------------------------------------------------------------------------------
1 | from src.common.logger import get_logger
2 | from src.config.config import model_config
3 | from src.llm_models.utils_model import LLMRequest
4 | from src.plugin_system.apis import send_api
5 |
6 | logger = get_logger(__name__)
7 |
8 | head_actions_list = ["不做额外动作", "点头一次", "点头两次", "摇头", "歪脑袋", "低头望向一边"]
9 |
10 |
11 | async def yes_or_no_head(text: str, emotion: str = "", chat_history: str = "", chat_id: str = ""):
12 | prompt = f"""
13 | {chat_history}
14 | 以上是对方的发言:
15 |
16 | 对这个发言,你的心情是:{emotion}
17 | 对上面的发言,你的回复是:{text}
18 | 请判断时是否要伴随回复做头部动作,你可以选择:
19 |
20 | 不做额外动作
21 | 点头一次
22 | 点头两次
23 | 摇头
24 | 歪脑袋
25 | 低头望向一边
26 |
27 | 请从上面的动作中选择一个,并输出,请只输出你选择的动作就好,不要输出其他内容。"""
28 | model = LLMRequest(model_set=model_config.model_task_config.emotion, request_type="motion")
29 |
30 | try:
31 | # logger.info(f"prompt: {prompt}")
32 | response, _ = await model.generate_response_async(prompt=prompt, temperature=0.7)
33 | logger.info(f"response: {response}")
34 |
35 | head_action = response if response in head_actions_list else "不做额外动作"
36 | await send_api.custom_to_stream(
37 | message_type="head_action",
38 | content=head_action,
39 | stream_id=chat_id,
40 | storage_message=False,
41 | show_log=True,
42 | )
43 |
44 | except Exception as e:
45 | logger.error(f"yes_or_no_head error: {e}")
46 | return "不做额外动作"
47 |
--------------------------------------------------------------------------------
/src/manager/local_store_manager.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import orjson
4 |
5 | from src.common.logger import get_logger
6 |
7 | LOCAL_STORE_FILE_PATH = "data/local_store.json"
8 |
9 | logger = get_logger("local_storage")
10 |
11 |
12 | class LocalStoreManager:
13 | file_path: str
14 | """本地存储路径"""
15 |
16 | store: dict[str, str | list | dict | int | float | bool]
17 | """本地存储数据"""
18 |
19 | def __init__(self, local_store_path: str | None = None):
20 | self.file_path = local_store_path or LOCAL_STORE_FILE_PATH
21 | self.store = {}
22 | self.load_local_store()
23 |
24 | def __getitem__(self, item: str) -> str | list | dict | int | float | bool | None:
25 | """获取本地存储数据"""
26 | return self.store.get(item)
27 |
28 | def __setitem__(self, key: str, value: str | list | dict | float | bool):
29 | """设置本地存储数据"""
30 | self.store[key] = value
31 | self.save_local_store()
32 |
33 | def __delitem__(self, key: str):
34 | """删除本地存储数据"""
35 | if key in self.store:
36 | del self.store[key]
37 | self.save_local_store()
38 | else:
39 | logger.warning(f"尝试删除不存在的键: {key}")
40 |
41 | def __contains__(self, item: str) -> bool:
42 | """检查本地存储数据是否存在"""
43 | return item in self.store
44 |
45 | def load_local_store(self):
46 | """加载本地存储数据"""
47 | if os.path.exists(self.file_path):
48 | # 存在本地存储文件,加载数据
49 | logger.info("正在阅读记事本......我在看,我真的在看!")
50 | logger.debug(f"加载本地存储数据: {self.file_path}")
51 | try:
52 | with open(self.file_path, encoding="utf-8") as f:
53 | self.store = orjson.loads(f.read())
54 | logger.info("全都记起来了!")
55 | except orjson.JSONDecodeError:
56 | logger.warning("啊咧?记事本被弄脏了,正在重建记事本......")
57 | self.store = {}
58 | with open(self.file_path, "w", encoding="utf-8") as f:
59 | f.write(orjson.dumps({}, option=orjson.OPT_INDENT_2).decode("utf-8"))
60 | logger.info("记事本重建成功!")
61 | else:
62 | # 不存在本地存储文件,创建新的目录和文件
63 | logger.warning("啊咧?记事本不存在,正在创建新的记事本......")
64 | os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
65 | with open(self.file_path, "w", encoding="utf-8") as f:
66 | f.write(orjson.dumps({}, option=orjson.OPT_INDENT_2).decode("utf-8"))
67 | logger.info("记事本创建成功!")
68 |
69 | def save_local_store(self):
70 | """保存本地存储数据"""
71 | logger.debug(f"保存本地存储数据: {self.file_path}")
72 | with open(self.file_path, "w", encoding="utf-8") as f:
73 | f.write(orjson.dumps(self.store, option=orjson.OPT_INDENT_2).decode("utf-8"))
74 |
75 |
76 | local_storage = LocalStoreManager("data/local_store.json") # 全局单例化
77 |
--------------------------------------------------------------------------------
/src/person_info/relationship_builder_manager.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | from src.common.logger import get_logger
4 |
5 | from .relationship_builder import RelationshipBuilder
6 |
7 | logger = get_logger("relationship_builder_manager")
8 |
9 |
10 | class RelationshipBuilderManager:
11 | """关系构建器管理器
12 |
13 | 简单的关系构建器存储和获取管理
14 | """
15 |
16 | def __init__(self):
17 | self.builders: dict[str, RelationshipBuilder] = {}
18 |
19 | def get_or_create_builder(self, chat_id: str) -> RelationshipBuilder:
20 | """获取或创建关系构建器
21 |
22 | Args:
23 | chat_id: 聊天ID
24 |
25 | Returns:
26 | RelationshipBuilder: 关系构建器实例
27 | """
28 | if chat_id not in self.builders:
29 | self.builders[chat_id] = RelationshipBuilder(chat_id)
30 | logger.debug(f"创建聊天 {chat_id} 的关系构建器")
31 |
32 | return self.builders[chat_id]
33 |
34 | def get_builder(self, chat_id: str) -> RelationshipBuilder | None:
35 | """获取关系构建器
36 |
37 | Args:
38 | chat_id: 聊天ID
39 |
40 | Returns:
41 | Optional[RelationshipBuilder]: 关系构建器实例或None
42 | """
43 | return self.builders.get(chat_id)
44 |
45 | def remove_builder(self, chat_id: str) -> bool:
46 | """移除关系构建器
47 |
48 | Args:
49 | chat_id: 聊天ID
50 |
51 | Returns:
52 | bool: 是否成功移除
53 | """
54 | if chat_id in self.builders:
55 | del self.builders[chat_id]
56 | logger.debug(f"移除聊天 {chat_id} 的关系构建器")
57 | return True
58 | return False
59 |
60 | def get_all_chat_ids(self) -> list[str]:
61 | """获取所有管理的聊天ID列表
62 |
63 | Returns:
64 | List[str]: 聊天ID列表
65 | """
66 | return list(self.builders.keys())
67 |
68 | def get_status(self) -> dict[str, Any]:
69 | """获取管理器状态
70 |
71 | Returns:
72 | Dict[str, any]: 状态信息
73 | """
74 | return {
75 | "total_builders": len(self.builders),
76 | "chat_ids": list(self.builders.keys()),
77 | }
78 |
79 | async def process_chat_messages(self, chat_id: str):
80 | """处理指定聊天的消息
81 |
82 | Args:
83 | chat_id: 聊天ID
84 | """
85 | builder = self.get_or_create_builder(chat_id)
86 | await builder.build_relation()
87 |
88 | async def force_cleanup_user(self, chat_id: str, person_id: str) -> bool:
89 | """强制清理指定用户的关系构建缓存
90 |
91 | Args:
92 | chat_id: 聊天ID
93 | person_id: 用户ID
94 |
95 | Returns:
96 | bool: 是否成功清理
97 | """
98 | builder = self.get_builder(chat_id)
99 | return builder.force_cleanup_user_segments(person_id) if builder else False
100 |
101 |
102 | # 全局管理器实例
103 | relationship_builder_manager = RelationshipBuilderManager()
104 |
--------------------------------------------------------------------------------
/src/plugin_system/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | MaiBot 插件系统
3 |
4 | 提供统一的插件开发和管理框架
5 | """
6 |
7 | # 导出主要的公共接口
8 | from .apis import (
9 | chat_api,
10 | component_manage_api,
11 | config_api,
12 | database_api,
13 | emoji_api,
14 | generator_api,
15 | get_logger,
16 | llm_api,
17 | message_api,
18 | person_api,
19 | plugin_manage_api,
20 | register_plugin,
21 | send_api,
22 | tool_api,
23 | )
24 | from .base import (
25 | ActionActivationType,
26 | ActionInfo,
27 | BaseAction,
28 | BaseCommand,
29 | BaseEventHandler,
30 | BasePlugin,
31 | BaseTool,
32 | ChatMode,
33 | ChatType,
34 | CommandArgs,
35 | CommandInfo,
36 | ComponentInfo,
37 | ComponentType,
38 | ConfigField,
39 | EventHandlerInfo,
40 | EventType,
41 | MaiMessages,
42 | PluginInfo,
43 | # 新增的增强命令系统
44 | PlusCommand,
45 | PlusCommandAdapter,
46 | PlusCommandInfo,
47 | PythonDependency,
48 | ToolInfo,
49 | ToolParamType,
50 | create_plus_command_adapter,
51 | )
52 | from .utils.dependency_config import configure_dependency_settings, get_dependency_config
53 |
54 | # 导入依赖管理模块
55 | from .utils.dependency_manager import configure_dependency_manager, get_dependency_manager
56 |
57 | __version__ = "2.0.0"
58 |
59 | __all__ = [
60 | "ActionActivationType",
61 | "ActionInfo",
62 | "BaseAction",
63 | "BaseCommand",
64 | "BaseEventHandler",
65 | # 基础类
66 | "BasePlugin",
67 | "BaseTool",
68 | "ChatMode",
69 | "ChatType",
70 | "CommandArgs",
71 | "CommandInfo",
72 | "ComponentInfo",
73 | # 类型定义
74 | "ComponentType",
75 | "ConfigField",
76 | "EventHandlerInfo",
77 | "EventType",
78 | # 消息
79 | "MaiMessages",
80 | # 工具函数
81 | "ManifestValidator",
82 | "PluginInfo",
83 | # 增强命令系统
84 | "PlusCommand",
85 | "PlusCommandAdapter",
86 | "PythonDependency",
87 | "ToolInfo",
88 | "ToolParamType",
89 | # API 模块
90 | "chat_api",
91 | "component_manage_api",
92 | "config_api",
93 | "configure_dependency_manager",
94 | "configure_dependency_settings",
95 | "create_plus_command_adapter",
96 | "create_plus_command_adapter",
97 | "database_api",
98 | "emoji_api",
99 | "generator_api",
100 | "get_dependency_config",
101 | # 依赖管理
102 | "get_dependency_manager",
103 | "get_logger",
104 | "get_logger",
105 | "llm_api",
106 | "message_api",
107 | "person_api",
108 | "plugin_manage_api",
109 | "register_plugin",
110 | # 装饰器
111 | "register_plugin",
112 | "send_api",
113 | "tool_api",
114 | # "ManifestGenerator",
115 | # "validate_plugin_manifest",
116 | # "generate_plugin_manifest",
117 | ]
118 |
--------------------------------------------------------------------------------
/src/plugin_system/apis/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 插件系统API模块
3 |
4 | 提供了插件开发所需的各种API
5 | """
6 |
7 | # 导入所有API模块
8 | from src.plugin_system.apis import (
9 | chat_api,
10 | component_manage_api,
11 | config_api,
12 | database_api,
13 | emoji_api,
14 | generator_api,
15 | llm_api,
16 | message_api,
17 | permission_api,
18 | person_api,
19 | plugin_manage_api,
20 | schedule_api,
21 | send_api,
22 | tool_api,
23 | )
24 | from src.plugin_system.apis.chat_api import ChatManager as context_api
25 |
26 | from .logging_api import get_logger
27 | from .plugin_register_api import register_plugin
28 |
29 | # 导出所有API模块,使它们可以通过 apis.xxx 方式访问
30 | __all__ = [
31 | "chat_api",
32 | "component_manage_api",
33 | "config_api",
34 | "context_api",
35 | "database_api",
36 | "emoji_api",
37 | "generator_api",
38 | "get_logger",
39 | "llm_api",
40 | "message_api",
41 | "permission_api",
42 | "person_api",
43 | "plugin_manage_api",
44 | "register_plugin",
45 | "schedule_api",
46 | "send_api",
47 | "tool_api",
48 | ]
49 |
--------------------------------------------------------------------------------
/src/plugin_system/apis/config_api.py:
--------------------------------------------------------------------------------
1 | """配置API模块
2 |
3 | 提供了配置读取和用户信息获取等功能
4 | 使用方式:
5 | from src.plugin_system.apis import config_api
6 | value = config_api.get_global_config("section.key")
7 | platform, user_id = await config_api.get_user_id_by_person_name("用户名")
8 | """
9 |
10 | from typing import Any
11 |
12 | from src.common.logger import get_logger
13 | from src.config.config import global_config
14 |
15 | logger = get_logger("config_api")
16 |
17 |
18 | # =============================================================================
19 | # 配置访问API函数
20 | # =============================================================================
21 |
22 |
23 | def get_global_config(key: str, default: Any = None) -> Any:
24 | """
25 | 安全地从全局配置中获取一个值。
26 | 插件应使用此方法读取全局配置,以保证只读和隔离性。
27 |
28 | Args:
29 | key: 命名空间式配置键名,使用嵌套访问,如 "section.subsection.key",大小写敏感
30 | default: 如果配置不存在时返回的默认值
31 |
32 | Returns:
33 | Any: 配置值或默认值
34 | """
35 | # 支持嵌套键访问
36 | keys = key.split(".")
37 | current = global_config
38 |
39 | try:
40 | for k in keys:
41 | if hasattr(current, k):
42 | current = getattr(current, k)
43 | else:
44 | raise KeyError(f"配置中不存在子空间或键 '{k}'")
45 | return current
46 | except Exception as e:
47 | logger.warning(f"[ConfigAPI] 获取全局配置 {key} 失败: {e}")
48 | return default
49 |
50 |
51 | def get_plugin_config(plugin_config: dict, key: str, default: Any = None) -> Any:
52 | """
53 | 从插件配置中获取值,支持嵌套键访问
54 |
55 | Args:
56 | plugin_config: 插件配置字典
57 | key: 配置键名,支持嵌套访问如 "section.subsection.key",大小写敏感
58 | default: 如果配置不存在时返回的默认值
59 |
60 | Returns:
61 | Any: 配置值或默认值
62 | """
63 | # 支持嵌套键访问
64 | keys = key.split(".")
65 | current = plugin_config
66 |
67 | try:
68 | for k in keys:
69 | if isinstance(current, dict) and k in current:
70 | current = current[k]
71 | elif hasattr(current, k):
72 | current = getattr(current, k)
73 | else:
74 | raise KeyError(f"配置中不存在子空间或键 '{k}'")
75 | return current
76 | except Exception as e:
77 | logger.warning(f"[ConfigAPI] 获取插件配置 {key} 失败: {e}")
78 | return default
79 |
--------------------------------------------------------------------------------
/src/plugin_system/apis/database_api.py:
--------------------------------------------------------------------------------
1 | """数据库API模块
2 |
3 | 提供数据库操作相关功能,采用标准Python包设计模式
4 | 使用方式:
5 | from src.plugin_system.apis import database_api
6 | records = await database_api.db_query(ActionRecords, query_type="get")
7 | record = await database_api.db_save(ActionRecords, data={"action_id": "123"})
8 |
9 | 注意:此模块现在使用SQLAlchemy实现,提供更好的连接管理和错误处理
10 | """
11 |
12 | from src.common.database.sqlalchemy_database_api import MODEL_MAPPING, db_get, db_query, db_save, store_action_info
13 |
14 | # 保持向后兼容性
15 | __all__ = ["MODEL_MAPPING", "db_get", "db_query", "db_save", "store_action_info"]
16 |
--------------------------------------------------------------------------------
/src/plugin_system/apis/logging_api.py:
--------------------------------------------------------------------------------
1 | from src.common.logger import get_logger
2 |
3 | __all__ = ["get_logger"]
4 |
--------------------------------------------------------------------------------
/src/plugin_system/apis/plugin_manage_api.py:
--------------------------------------------------------------------------------
1 | def list_loaded_plugins() -> list[str]:
2 | """
3 | 列出所有当前加载的插件。
4 |
5 | Returns:
6 | List[str]: 当前加载的插件名称列表。
7 | """
8 | from src.plugin_system.core.plugin_manager import plugin_manager
9 |
10 | return plugin_manager.list_loaded_plugins()
11 |
12 |
13 | def list_registered_plugins() -> list[str]:
14 | """
15 | 列出所有已注册的插件。
16 |
17 | Returns:
18 | List[str]: 已注册的插件名称列表。
19 | """
20 | from src.plugin_system.core.plugin_manager import plugin_manager
21 |
22 | return plugin_manager.list_registered_plugins()
23 |
24 |
25 | def get_plugin_path(plugin_name: str) -> str:
26 | """
27 | 获取指定插件的路径。
28 |
29 | Args:
30 | plugin_name (str): 插件名称。
31 |
32 | Returns:
33 | str: 插件目录的绝对路径。
34 |
35 | Raises:
36 | ValueError: 如果插件不存在。
37 | """
38 | from src.plugin_system.core.plugin_manager import plugin_manager
39 |
40 | if plugin_path := plugin_manager.get_plugin_path(plugin_name):
41 | return plugin_path
42 | else:
43 | raise ValueError(f"插件 '{plugin_name}' 不存在。")
44 |
45 |
46 | async def remove_plugin(plugin_name: str) -> bool:
47 | """
48 | 卸载指定的插件。
49 |
50 | **此函数是异步的,确保在异步环境中调用。**
51 |
52 | Args:
53 | plugin_name (str): 要卸载的插件名称。
54 |
55 | Returns:
56 | bool: 卸载是否成功。
57 | """
58 | from src.plugin_system.core.plugin_manager import plugin_manager
59 |
60 | return await plugin_manager.remove_registered_plugin(plugin_name)
61 |
62 |
63 | async def reload_plugin(plugin_name: str) -> bool:
64 | """
65 | 重新加载指定的插件。
66 |
67 | **此函数是异步的,确保在异步环境中调用。**
68 |
69 | Args:
70 | plugin_name (str): 要重新加载的插件名称。
71 |
72 | Returns:
73 | bool: 重新加载是否成功。
74 | """
75 | from src.plugin_system.core.plugin_manager import plugin_manager
76 |
77 | return await plugin_manager.reload_registered_plugin(plugin_name)
78 |
79 |
80 | def load_plugin(plugin_name: str) -> tuple[bool, int]:
81 | """
82 | 加载指定的插件。
83 |
84 | Args:
85 | plugin_name (str): 要加载的插件名称。
86 |
87 | Returns:
88 | Tuple[bool, int]: 加载是否成功,成功或失败个数。
89 | """
90 | from src.plugin_system.core.plugin_manager import plugin_manager
91 |
92 | return plugin_manager.load_registered_plugin_classes(plugin_name)
93 |
94 |
95 | def add_plugin_directory(plugin_directory: str) -> bool:
96 | """
97 | 添加插件目录。
98 |
99 | Args:
100 | plugin_directory (str): 要添加的插件目录路径。
101 | Returns:
102 | bool: 添加是否成功。
103 | """
104 | from src.plugin_system.core.plugin_manager import plugin_manager
105 |
106 | return plugin_manager.add_plugin_directory(plugin_directory)
107 |
108 |
109 | def rescan_plugin_directory() -> tuple[int, int]:
110 | """
111 | 重新扫描插件目录,加载新插件。
112 | Returns:
113 | Tuple[int, int]: 成功加载的插件数量和失败的插件数量。
114 | """
115 | from src.plugin_system.core.plugin_manager import plugin_manager
116 |
117 | return plugin_manager.rescan_plugin_directory()
118 |
--------------------------------------------------------------------------------
/src/plugin_system/apis/plugin_register_api.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | from src.common.logger import get_logger
4 |
5 | logger = get_logger("plugin_manager") # 复用plugin_manager名称
6 |
7 |
8 | def register_plugin(cls):
9 | from src.plugin_system.base.base_plugin import BasePlugin
10 | from src.plugin_system.core.plugin_manager import plugin_manager
11 |
12 | """插件注册装饰器
13 |
14 | 用法:
15 | @register_plugin
16 | class MyPlugin(BasePlugin):
17 | plugin_name = "my_plugin"
18 | plugin_description = "我的插件"
19 | ...
20 | """
21 | if not issubclass(cls, BasePlugin):
22 | logger.error(f"类 {cls.__name__} 不是 BasePlugin 的子类")
23 | return cls
24 |
25 | # 只是注册插件类,不立即实例化
26 | # 插件管理器会负责实例化和注册
27 | plugin_name: str = cls.plugin_name # type: ignore
28 | if "." in plugin_name:
29 | logger.error(f"插件名称 '{plugin_name}' 包含非法字符 '.',请使用下划线替代")
30 | raise ValueError(f"插件名称 '{plugin_name}' 包含非法字符 '.',请使用下划线替代")
31 | splitted_name = cls.__module__.split(".")
32 | root_path = Path(__file__)
33 |
34 | # 查找项目根目录
35 | while not (root_path / "pyproject.toml").exists() and root_path.parent != root_path:
36 | root_path = root_path.parent
37 |
38 | if not (root_path / "pyproject.toml").exists():
39 | logger.error(f"注册 {plugin_name} 无法找到项目根目录")
40 | return cls
41 |
42 | plugin_manager.plugin_classes[plugin_name] = cls
43 | plugin_manager.plugin_paths[plugin_name] = str(Path(root_path, *splitted_name).resolve())
44 | logger.debug(f"插件类已注册: {plugin_name}, 路径: {plugin_manager.plugin_paths[plugin_name]}")
45 |
46 | return cls
47 |
--------------------------------------------------------------------------------
/src/plugin_system/apis/scoring_api.py:
--------------------------------------------------------------------------------
1 | """
2 | 统一评分系统API
3 | 提供系统级的关系分和兴趣管理服务,供所有插件和主项目组件使用
4 | """
5 |
6 | from typing import Any
7 |
8 | from src.common.logger import get_logger
9 | from src.plugin_system.services.interest_service import interest_service
10 | from src.plugin_system.services.relationship_service import relationship_service
11 |
12 | logger = get_logger("scoring_api")
13 |
14 |
15 | class ScoringAPI:
16 | """
17 | 统一评分系统API - 系统级服务
18 |
19 | 提供关系分和兴趣管理的统一接口,替代原有的插件依赖方式。
20 | 所有插件和主项目组件都应该通过此API访问评分功能。
21 | """
22 |
23 | @staticmethod
24 | async def get_user_relationship_score(user_id: str) -> float:
25 | """
26 | 获取用户关系分
27 |
28 | Args:
29 | user_id: 用户ID
30 |
31 | Returns:
32 | 关系分 (0.0 - 1.0)
33 | """
34 | return await relationship_service.get_user_relationship_score(user_id)
35 |
36 | @staticmethod
37 | async def get_user_relationship_data(user_id: str) -> dict:
38 | """
39 | 获取用户完整关系数据
40 |
41 | Args:
42 | user_id: 用户ID
43 |
44 | Returns:
45 | 包含关系分、关系文本等的字典
46 | """
47 | return await relationship_service.get_user_relationship_data(user_id)
48 |
49 | @staticmethod
50 | async def update_user_relationship(user_id: str, relationship_score: float, relationship_text: str = None, user_name: str = None):
51 | """
52 | 更新用户关系数据
53 |
54 | Args:
55 | user_id: 用户ID
56 | relationship_score: 关系分 (0.0 - 1.0)
57 | relationship_text: 关系描述文本
58 | user_name: 用户名称
59 | """
60 | await relationship_service.update_user_relationship(user_id, relationship_score, relationship_text, user_name)
61 |
62 | @staticmethod
63 | async def initialize_smart_interests(personality_description: str, personality_id: str = "default"):
64 | """
65 | 初始化智能兴趣系统
66 |
67 | Args:
68 | personality_description: 机器人性格描述
69 | personality_id: 性格ID
70 | """
71 | await interest_service.initialize_smart_interests(personality_description, personality_id)
72 |
73 | @staticmethod
74 | async def calculate_interest_match(content: str, keywords: list[str] = None):
75 | """
76 | 计算内容与兴趣的匹配度
77 |
78 | Args:
79 | content: 消息内容
80 | keywords: 关键词列表
81 |
82 | Returns:
83 | 匹配结果
84 | """
85 | return await interest_service.calculate_interest_match(content, keywords)
86 |
87 | @staticmethod
88 | def get_system_stats() -> dict[str, Any]:
89 | """
90 | 获取系统统计信息
91 |
92 | Returns:
93 | 包含各子系统统计的字典
94 | """
95 | return {
96 | "relationship_service": relationship_service.get_cache_stats(),
97 | "interest_service": interest_service.get_interest_stats()
98 | }
99 |
100 | @staticmethod
101 | def clear_caches(user_id: str = None):
102 | """
103 | 清理缓存
104 |
105 | Args:
106 | user_id: 特定用户ID,如果为None则清理所有缓存
107 | """
108 | relationship_service.clear_cache(user_id)
109 | logger.info(f"清理缓存: {user_id if user_id else '全部'}")
110 |
111 |
112 | # 创建全局API实例 - 系统级服务
113 | scoring_api = ScoringAPI()
114 |
--------------------------------------------------------------------------------
/src/plugin_system/apis/tool_api.py:
--------------------------------------------------------------------------------
1 | from src.common.logger import get_logger
2 | from src.plugin_system.base.base_tool import BaseTool
3 | from src.plugin_system.base.component_types import ComponentType
4 |
5 | logger = get_logger("tool_api")
6 |
7 |
8 | def get_tool_instance(tool_name: str) -> BaseTool | None:
9 | """获取公开工具实例"""
10 | from src.plugin_system.core import component_registry
11 |
12 | # 获取插件配置
13 | tool_info = component_registry.get_component_info(tool_name, ComponentType.TOOL)
14 | if tool_info:
15 | plugin_config = component_registry.get_plugin_config(tool_info.plugin_name)
16 | else:
17 | plugin_config = None
18 |
19 | tool_class: type[BaseTool] = component_registry.get_component_class(tool_name, ComponentType.TOOL) # type: ignore
20 | if tool_class:
21 | return tool_class(plugin_config)
22 |
23 | # 如果不是常规工具,检查是否是MCP工具
24 | # MCP工具不需要返回实例,会在execute_tool_call中特殊处理
25 | return None
26 |
27 |
28 | def get_llm_available_tool_definitions():
29 | """获取LLM可用的工具定义列表
30 |
31 | Returns:
32 | List[Tuple[str, Dict[str, Any]]]: 工具定义列表,为[("tool_name", 定义)]
33 | """
34 | from src.plugin_system.core import component_registry
35 |
36 | llm_available_tools = component_registry.get_llm_available_tools()
37 | tool_definitions = [(name, tool_class.get_tool_definition()) for name, tool_class in llm_available_tools.items()]
38 |
39 | # 添加MCP工具
40 | try:
41 | from src.plugin_system.utils.mcp_tool_provider import mcp_tool_provider
42 |
43 | mcp_tools = mcp_tool_provider.get_mcp_tool_definitions()
44 | tool_definitions.extend(mcp_tools)
45 | if mcp_tools:
46 | logger.debug(f"已添加 {len(mcp_tools)} 个MCP工具到可用工具列表")
47 | except Exception as e:
48 | logger.debug(f"获取MCP工具失败(可能未配置): {e}")
49 |
50 | return tool_definitions
51 |
--------------------------------------------------------------------------------
/src/plugin_system/base/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 插件基础类模块
3 |
4 | 提供插件开发的基础类和类型定义
5 | """
6 |
7 | from .base_action import BaseAction
8 | from .base_command import BaseCommand
9 | from .base_events_handler import BaseEventHandler
10 | from .base_plugin import BasePlugin
11 | from .base_tool import BaseTool
12 | from .command_args import CommandArgs
13 | from .component_types import (
14 | ActionActivationType,
15 | ActionInfo,
16 | ChatMode,
17 | ChatType,
18 | CommandInfo,
19 | ComponentInfo,
20 | ComponentType,
21 | EventHandlerInfo,
22 | EventType,
23 | MaiMessages,
24 | PluginInfo,
25 | PlusCommandInfo,
26 | PythonDependency,
27 | ToolInfo,
28 | ToolParamType,
29 | )
30 | from .config_types import ConfigField
31 | from .plus_command import PlusCommand, PlusCommandAdapter, create_plus_command_adapter
32 |
33 | __all__ = [
34 | "ActionActivationType",
35 | "ActionInfo",
36 | "BaseAction",
37 | "BaseCommand",
38 | "BaseEventHandler",
39 | "BasePlugin",
40 | "BaseTool",
41 | "ChatMode",
42 | "ChatType",
43 | "CommandArgs",
44 | "CommandInfo",
45 | "ComponentInfo",
46 | "ComponentType",
47 | "ConfigField",
48 | "EventHandlerInfo",
49 | "EventType",
50 | "MaiMessages",
51 | "PluginInfo",
52 | # 增强命令系统
53 | "PlusCommand",
54 | "PlusCommandAdapter",
55 | "PlusCommandInfo",
56 | "PythonDependency",
57 | "ToolInfo",
58 | "ToolParamType",
59 | "create_plus_command_adapter",
60 | ]
61 |
--------------------------------------------------------------------------------
/src/plugin_system/base/base_chatter.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | from typing import TYPE_CHECKING
3 |
4 | from src.common.data_models.message_manager_data_model import StreamContext
5 | from src.plugin_system.base.component_types import ChatterInfo, ComponentType
6 |
7 | from .component_types import ChatType
8 |
9 | if TYPE_CHECKING:
10 | from src.chat.planner_actions.action_manager import ChatterActionManager
11 |
12 |
13 | class BaseChatter(ABC):
14 | chatter_name: str = ""
15 | """Chatter组件的名称"""
16 | chatter_description: str = ""
17 | """Chatter组件的描述"""
18 | chat_types: list[ChatType] = [ChatType.PRIVATE, ChatType.GROUP]
19 |
20 | def __init__(self, stream_id: str, action_manager: "ChatterActionManager"):
21 | """
22 | 初始化聊天处理器
23 |
24 | Args:
25 | stream_id: 聊天流ID
26 | action_manager: 动作管理器
27 | """
28 | self.stream_id = stream_id
29 | self.action_manager = action_manager
30 |
31 | @abstractmethod
32 | async def execute(self, context: StreamContext) -> dict:
33 | """
34 | 执行聊天处理流程
35 |
36 | Args:
37 | context: StreamContext对象,包含聊天流的所有消息信息
38 |
39 | Returns:
40 | 处理结果字典
41 | """
42 | pass
43 |
44 | @classmethod
45 | def get_chatter_info(cls) -> "ChatterInfo":
46 | """从类属性生成ChatterInfo
47 | Returns:
48 | ChatterInfo对象
49 | """
50 |
51 | return ChatterInfo(
52 | name=cls.chatter_name,
53 | description=cls.chatter_description or "No description provided.",
54 | chat_type_allow=cls.chat_types[0],
55 | component_type=ComponentType.CHATTER,
56 | )
57 |
--------------------------------------------------------------------------------
/src/plugin_system/base/config_types.py:
--------------------------------------------------------------------------------
1 | """
2 | 插件系统配置类型定义
3 | """
4 |
5 | from dataclasses import dataclass, field
6 | from typing import Any
7 |
8 |
9 | @dataclass
10 | class ConfigField:
11 | """配置字段定义"""
12 |
13 | type: type # 字段类型
14 | default: Any # 默认值
15 | description: str # 字段描述
16 | example: str | None = None # 示例值
17 | required: bool = False # 是否必需
18 | choices: list[Any] | None = field(default_factory=list) # 可选值列表
19 |
--------------------------------------------------------------------------------
/src/plugin_system/base/plugin_metadata.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass, field
2 | from typing import Any
3 |
4 |
5 | @dataclass
6 | class PluginMetadata:
7 | """
8 | 插件元数据,用于存储插件的开发者信息和用户帮助信息。
9 | """
10 |
11 | name: str # 插件名称 (供用户查看)
12 | description: str # 插件功能描述
13 | usage: str # 插件使用方法
14 |
15 | # 以下为可选字段,参考自 _manifest.json 和 NoneBot 设计
16 | type: str | None = None # 插件类别: "library", "application"
17 |
18 | # 从原 _manifest.json 迁移的字段
19 | version: str = "1.0.0" # 插件版本
20 | author: str = "" # 作者名称
21 | license: str | None = None # 开源协议
22 | repository_url: str | None = None # 仓库地址
23 | keywords: list[str] = field(default_factory=list) # 关键词
24 | categories: list[str] = field(default_factory=list) # 分类
25 |
26 | # 扩展字段
27 | extra: dict[str, Any] = field(default_factory=dict) # 其他任意信息
28 |
--------------------------------------------------------------------------------
/src/plugin_system/core/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 插件核心管理模块
3 |
4 | 提供插件的加载、注册和管理功能
5 | """
6 |
7 | from src.plugin_system.core.component_registry import component_registry
8 | from src.plugin_system.core.event_manager import event_manager
9 | from src.plugin_system.core.global_announcement_manager import global_announcement_manager
10 | from src.plugin_system.core.plugin_manager import plugin_manager
11 |
12 | __all__ = [
13 | "component_registry",
14 | "event_manager",
15 | "global_announcement_manager",
16 | "plugin_manager",
17 | ]
18 |
--------------------------------------------------------------------------------
/src/plugin_system/utils/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 插件系统工具模块
3 |
4 | 提供插件开发和管理的实用工具
5 | """
6 |
--------------------------------------------------------------------------------
/src/plugin_system/utils/dependency_config.py:
--------------------------------------------------------------------------------
1 | from src.common.logger import get_logger
2 |
3 | logger = get_logger("dependency_config")
4 |
5 |
6 | class DependencyConfig:
7 | """依赖管理配置类 - 现在使用全局配置"""
8 |
9 | def __init__(self, global_config=None):
10 | self._global_config = global_config
11 |
12 | def _get_config(self):
13 | """获取全局配置对象"""
14 | if self._global_config is not None:
15 | return self._global_config
16 |
17 | # 延迟导入以避免循环依赖
18 | try:
19 | from src.config.config import global_config
20 |
21 | return global_config
22 | except ImportError:
23 | logger.warning("无法导入全局配置,使用默认设置")
24 | return None
25 |
26 | @property
27 | def auto_install(self) -> bool:
28 | """是否启用自动安装"""
29 | config = self._get_config()
30 | if config and hasattr(config, "dependency_management"):
31 | return config.dependency_management.auto_install
32 | return True
33 |
34 | @property
35 | def use_mirror(self) -> bool:
36 | """是否使用PyPI镜像源"""
37 | config = self._get_config()
38 | if config and hasattr(config, "dependency_management"):
39 | return config.dependency_management.use_mirror
40 | return False
41 |
42 | @property
43 | def mirror_url(self) -> str:
44 | """PyPI镜像源URL"""
45 | config = self._get_config()
46 | if config and hasattr(config, "dependency_management"):
47 | return config.dependency_management.mirror_url
48 | return ""
49 |
50 | @property
51 | def install_timeout(self) -> int:
52 | """安装超时时间(秒)"""
53 | config = self._get_config()
54 | if config and hasattr(config, "dependency_management"):
55 | return config.dependency_management.auto_install_timeout
56 | return 300
57 |
58 | @property
59 | def prompt_before_install(self) -> bool:
60 | """安装前是否提示用户"""
61 | config = self._get_config()
62 | if config and hasattr(config, "dependency_management"):
63 | return config.dependency_management.prompt_before_install
64 | return False
65 |
66 |
67 | # 全局配置实例
68 | _global_dependency_config: DependencyConfig | None = None
69 |
70 |
71 | def get_dependency_config() -> DependencyConfig:
72 | """获取全局依赖配置实例"""
73 | global _global_dependency_config
74 | if _global_dependency_config is None:
75 | _global_dependency_config = DependencyConfig()
76 | return _global_dependency_config
77 |
78 |
79 | def configure_dependency_settings(**kwargs) -> None:
80 | """配置依赖管理设置 - 注意:这个函数现在仅用于兼容性,实际配置需要修改bot_config.toml"""
81 | logger.info("依赖管理设置现在通过 bot_config.toml 的 [dependency_management] 节进行配置")
82 | logger.info(f"请求的配置更改: {kwargs}")
83 | logger.warning("configure_dependency_settings 函数仅用于兼容性,配置更改不会持久化")
84 |
--------------------------------------------------------------------------------
/src/plugins/__init__.py:
--------------------------------------------------------------------------------
1 | """插件系统包"""
2 |
--------------------------------------------------------------------------------
/src/plugins/built_in/affinity_flow_chatter/README.md:
--------------------------------------------------------------------------------
1 | # 亲和力聊天处理器插件
2 |
3 | ## 概述
4 |
5 | 这是一个内置的chatter插件,实现了基于亲和力流的智能聊天处理器,具有兴趣度评分和人物关系构建功能。
6 |
7 | ## 功能特性
8 |
9 | - **智能兴趣度评分**: 自动识别和评估用户兴趣话题
10 | - **人物关系系统**: 根据互动历史建立和维持用户关系
11 | - **多聊天类型支持**: 支持私聊和群聊场景
12 | - **插件化架构**: 完全集成到插件系统中
13 |
14 | ## 组件架构
15 |
16 | ### BaseChatter (抽象基类)
17 | - 位置: `src/plugin_system/base/base_chatter.py`
18 | - 功能: 定义所有chatter组件的基础接口
19 | - 必须实现的方法: `execute(context: StreamContext) -> dict`
20 |
21 | ### ChatterManager (管理器)
22 | - 位置: `src/chat/chatter_manager.py`
23 | - 功能: 管理和调度所有chatter组件
24 | - 特性: 自动从插件系统注册和发现chatter组件
25 |
26 | ### AffinityChatter (具体实现)
27 | - 位置: `src/plugins/built_in/chatter/affinity_chatter.py`
28 | - 功能: 亲和力流聊天处理器的具体实现
29 | - 支持的聊天类型: PRIVATE, GROUP
30 |
31 | ## 使用方法
32 |
33 | ### 1. 基本使用
34 |
35 | ```python
36 | from src.chat.chatter_manager import ChatterManager
37 | from src.chat.planner_actions.action_manager import ChatterActionManager
38 |
39 | # 初始化
40 | action_manager = ChatterActionManager()
41 | chatter_manager = ChatterManager(action_manager)
42 |
43 | # 处理消息流
44 | result = await chatter_manager.process_stream_context(stream_id, context)
45 | ```
46 |
47 | ### 2. 创建自定义Chatter
48 |
49 | ```python
50 | from src.plugin_system.base.base_chatter import BaseChatter
51 | from src.plugin_system.base.component_types import ChatType, ComponentType
52 | from src.plugin_system.base.component_types import ChatterInfo
53 |
54 | class CustomChatter(BaseChatter):
55 | chat_types = [ChatType.PRIVATE] # 只支持私聊
56 |
57 | async def execute(self, context: StreamContext) -> dict:
58 | # 实现你的聊天逻辑
59 | return {"success": True, "message": "处理完成"}
60 |
61 | # 在插件中注册
62 | async def on_load(self):
63 | chatter_info = ChatterInfo(
64 | name="custom_chatter",
65 | component_type=ComponentType.CHATTER,
66 | description="自定义聊天处理器",
67 | enabled=True,
68 | plugin_name=self.name,
69 | chat_type_allow=ChatType.PRIVATE
70 | )
71 |
72 | ComponentRegistry.register_component(
73 | component_info=chatter_info,
74 | component_class=CustomChatter
75 | )
76 | ```
77 |
78 | ## 配置
79 |
80 | ### 插件配置文件
81 | - 位置: `src/plugins/built_in/chatter/_manifest.json`
82 | - 包含插件信息和组件配置
83 |
84 | ### 聊天类型
85 | - `PRIVATE`: 私聊
86 | - `GROUP`: 群聊
87 | - `ALL`: 所有类型
88 |
89 | ## 核心概念
90 |
91 | ### 1. 兴趣值系统
92 | - 自动识别同类话题
93 | - 兴趣值会根据聊天频率增减
94 | - 支持新话题的自动学习
95 |
96 | ### 2. 人物关系系统
97 | - 根据互动质量建立关系分
98 | - 不同关系分对应不同的回复风格
99 | - 支持情感化的交流
100 |
101 | ### 3. 执行流程
102 | 1. 接收StreamContext
103 | 2. 使用ActionPlanner进行规划
104 | 3. 执行相应的Action
105 | 4. 返回处理结果
106 |
107 | ## 扩展开发
108 |
109 | ### 添加新的Chatter类型
110 | 1. 继承BaseChatter类
111 | 2. 实现execute方法
112 | 3. 在插件中注册组件
113 | 4. 配置支持的聊天类型
114 |
115 | ### 集成现有功能
116 | - 使用ActionPlanner进行动作规划
117 | - 通过ActionManager执行动作
118 | - 利用现有的记忆和知识系统
119 |
120 | ## 注意事项
121 |
122 | 1. 所有chatter组件必须实现`execute`方法
123 | 2. 插件注册时需要指定支持的聊天类型
124 | 3. 组件名称不能包含点号(.)
125 | 4. 确保在插件卸载时正确清理资源
--------------------------------------------------------------------------------
/src/plugins/built_in/affinity_flow_chatter/__init__.py:
--------------------------------------------------------------------------------
1 | from src.plugin_system.base.plugin_metadata import PluginMetadata
2 |
3 | __plugin_meta__ = PluginMetadata(
4 | name="Affinity Flow Chatter",
5 | description="Built-in chatter plugin for affinity flow with interest scoring and relationship building",
6 | usage="This plugin is automatically triggered by the system.",
7 | version="1.0.0",
8 | author="MoFox",
9 | keywords=["chatter", "affinity", "conversation"],
10 | categories=["Chat", "AI"],
11 | extra={"is_built_in": True},
12 | )
13 |
--------------------------------------------------------------------------------
/src/plugins/built_in/affinity_flow_chatter/plugin.py:
--------------------------------------------------------------------------------
1 | """
2 | 亲和力聊天处理器插件(包含兴趣计算器功能)
3 | """
4 |
5 | from src.common.logger import get_logger
6 | from src.plugin_system.apis.plugin_register_api import register_plugin
7 | from src.plugin_system.base.base_plugin import BasePlugin
8 | from src.plugin_system.base.component_types import ComponentInfo
9 |
10 | logger = get_logger("affinity_chatter_plugin")
11 |
12 |
13 | @register_plugin
14 | class AffinityChatterPlugin(BasePlugin):
15 | """亲和力聊天处理器插件
16 |
17 | - 延迟导入 `AffinityChatter` 并通过组件注册器注册为聊天处理器
18 | - 延迟导入 `AffinityInterestCalculator` 并通过组件注册器注册为兴趣计算器
19 | - 提供 `get_plugin_components` 以兼容插件注册机制
20 | """
21 |
22 | plugin_name: str = "affinity_chatter"
23 | enable_plugin: bool = True
24 | dependencies: list[str] = []
25 | python_dependencies: list[str] = []
26 | config_file_name: str = ""
27 |
28 | # 简单的 config_schema 占位(如果将来需要配置可扩展)
29 | config_schema = {}
30 |
31 | def get_plugin_components(self) -> list[tuple[ComponentInfo, type]]:
32 | """返回插件包含的组件列表
33 |
34 | 这里采用延迟导入以避免循环依赖和启动顺序问题。
35 | 如果导入失败则返回空列表以让注册过程继续而不崩溃。
36 | """
37 | components = []
38 |
39 | try:
40 | # 延迟导入 AffinityChatter
41 | from .affinity_chatter import AffinityChatter
42 |
43 | components.append((AffinityChatter.get_chatter_info(), AffinityChatter))
44 | except Exception as e:
45 | logger.error(f"加载 AffinityChatter 时出错: {e}")
46 |
47 | try:
48 | # 延迟导入 AffinityInterestCalculator
49 | from .affinity_interest_calculator import AffinityInterestCalculator
50 |
51 | components.append((AffinityInterestCalculator.get_interest_calculator_info(), AffinityInterestCalculator))
52 | except Exception as e:
53 | logger.error(f"加载 AffinityInterestCalculator 时出错: {e}")
54 |
55 | return components
56 |
--------------------------------------------------------------------------------
/src/plugins/built_in/core_actions/__init__.py:
--------------------------------------------------------------------------------
1 | from src.plugin_system.base.plugin_metadata import PluginMetadata
2 |
3 | __plugin_meta__ = PluginMetadata(
4 | name="Emoji插件 (Emoji Actions)",
5 | description="可以发送和管理Emoji",
6 | usage="该插件提供 `emoji` action。",
7 | version="1.0.0",
8 | author="SengokuCola",
9 | license="GPL-v3.0-or-later",
10 | repository_url="https://github.com/MaiM-with-u/maibot",
11 | keywords=["emoji", "action", "built-in"],
12 | categories=["Emoji"],
13 | extra={
14 | "is_built_in": True,
15 | "plugin_type": "action_provider",
16 | },
17 | )
18 |
--------------------------------------------------------------------------------
/src/plugins/built_in/core_actions/anti_injector_manager.py:
--------------------------------------------------------------------------------
1 | """
2 | 反注入系统管理命令插件
3 |
4 | 提供管理和监控反注入系统的命令接口,包括:
5 | - 系统状态查看
6 | - 配置修改
7 | - 统计信息查看
8 | - 测试功能
9 | """
10 |
11 | from src.chat.antipromptinjector import get_anti_injector
12 | from src.common.logger import get_logger
13 | from src.plugin_system.base import BaseCommand
14 |
15 | logger = get_logger("anti_injector.commands")
16 |
17 |
18 | class AntiInjectorStatusCommand(BaseCommand):
19 | """反注入系统状态查看命令"""
20 |
21 | command_name = "反注入状态" # 命令名称,作为唯一标识符
22 | command_description = "查看反注入系统状态和统计信息" # 命令描述
23 | command_pattern = r"^/反注入状态$" # 命令匹配的正则表达式
24 |
25 | async def execute(self) -> tuple[bool, str, bool]:
26 | try:
27 | anti_injector = get_anti_injector()
28 | stats = await anti_injector.get_stats()
29 |
30 | # 检查反注入系统是否禁用
31 | if stats.get("status") == "disabled":
32 | await self.send_text("❌ 反注入系统未启用\n\n💡 请在配置文件中启用反注入功能后重试")
33 | return True, "反注入系统未启用", True
34 |
35 | if stats.get("error"):
36 | await self.send_text(f"❌ 获取状态失败: {stats['error']}")
37 | return False, f"获取状态失败: {stats['error']}", True
38 |
39 | status_text = f"""🛡️ 反注入系统状态报告
40 |
41 | 📊 运行统计:
42 | • 运行时间: {stats["uptime"]}
43 | • 处理消息总数: {stats["total_messages"]}
44 | • 检测到注入: {stats["detected_injections"]}
45 | • 阻止消息: {stats["blocked_messages"]}
46 | • 加盾消息: {stats["shielded_messages"]}
47 |
48 | 📈 性能指标:
49 | • 检测率: {stats["detection_rate"]}
50 | • 平均处理时间: {stats["average_processing_time"]}
51 | • 最后处理时间: {stats["last_processing_time"]}
52 |
53 | ⚠️ 错误计数: {stats["error_count"]}"""
54 | await self.send_text(status_text)
55 | return True, status_text, True
56 |
57 | except Exception as e:
58 | logger.error(f"获取反注入系统状态失败: {e}")
59 | await self.send_text(f"获取状态失败: {e!s}")
60 | return False, f"获取状态失败: {e!s}", True
61 |
--------------------------------------------------------------------------------
/src/plugins/built_in/core_actions/plugin.py:
--------------------------------------------------------------------------------
1 | """
2 | 核心动作插件
3 |
4 | 将系统核心动作(reply、no_reply、emoji)转换为新插件系统格式
5 | 这是系统的内置插件,提供基础的聊天交互功能
6 | """
7 |
8 | # 导入依赖的系统组件
9 | from src.common.logger import get_logger
10 |
11 | # 导入新插件系统
12 | from src.plugin_system import BasePlugin, ComponentInfo, register_plugin
13 | from src.plugin_system.base.config_types import ConfigField
14 | from src.plugins.built_in.core_actions.anti_injector_manager import AntiInjectorStatusCommand
15 |
16 | # 导入API模块 - 标准Python包方式
17 | from src.plugins.built_in.core_actions.emoji import EmojiAction
18 |
19 | logger = get_logger("core_actions")
20 |
21 |
22 | @register_plugin
23 | class CoreActionsPlugin(BasePlugin):
24 | """核心动作插件
25 |
26 | 系统内置插件,提供基础的聊天交互功能:
27 | - Reply: 回复动作
28 | - NoReply: 不回复动作
29 | - Emoji: 表情动作
30 |
31 | 注意:插件基本信息优先从_manifest.json文件中读取
32 | """
33 |
34 | # 插件基本信息
35 | plugin_name: str = "core_actions" # 内部标识符
36 | enable_plugin: bool = True
37 | dependencies: list[str] = [] # 插件依赖列表
38 | python_dependencies: list[str] = [] # Python包依赖列表
39 | config_file_name: str = "config.toml"
40 |
41 | # 配置节描述
42 | config_section_descriptions = {
43 | "plugin": "插件启用配置",
44 | "components": "核心组件启用配置",
45 | }
46 |
47 | # 配置Schema定义
48 | config_schema: dict = {
49 | "plugin": {
50 | "enabled": ConfigField(type=bool, default=True, description="是否启用插件"),
51 | "config_version": ConfigField(type=str, default="0.6.0", description="配置文件版本"),
52 | },
53 | "components": {
54 | "enable_reply": ConfigField(type=bool, default=True, description="是否启用基本回复动作"),
55 | "enable_emoji": ConfigField(type=bool, default=True, description="是否启用发送表情/图片动作"),
56 | "enable_anti_injector_manager": ConfigField(
57 | type=bool, default=True, description="是否启用反注入系统管理命令"
58 | ),
59 | },
60 | }
61 |
62 | def get_plugin_components(self) -> list[tuple[ComponentInfo, type]]:
63 | """返回插件包含的组件列表"""
64 |
65 | # --- 根据配置注册组件 ---
66 | components = []
67 | if self.get_config("components.enable_emoji", True):
68 | components.append((EmojiAction.get_action_info(), EmojiAction))
69 | if self.get_config("components.enable_anti_injector_manager", True):
70 | components.append((AntiInjectorStatusCommand.get_command_info(), AntiInjectorStatusCommand))
71 |
72 | return components
73 |
--------------------------------------------------------------------------------
/src/plugins/built_in/knowledge/lpmm_get_knowledge.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | from src.chat.knowledge.knowledge_lib import qa_manager
4 | from src.common.logger import get_logger
5 | from src.config.config import global_config
6 | from src.plugin_system import BaseTool, ToolParamType
7 |
8 | logger = get_logger("lpmm_get_knowledge_tool")
9 |
10 |
11 | class SearchKnowledgeFromLPMMTool(BaseTool):
12 | """从LPMM知识库中搜索相关信息的工具"""
13 |
14 | name = "lpmm_search_knowledge"
15 | description = "从知识库中搜索相关信息,如果你需要知识,就使用这个工具"
16 | parameters = [
17 | ("query", ToolParamType.STRING, "搜索查询关键词", True, None),
18 | ("threshold", ToolParamType.FLOAT, "相似度阈值,0.0到1.0之间", False, None),
19 | ]
20 | available_for_llm = global_config.lpmm_knowledge.enable
21 |
22 | async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]:
23 | """执行知识库搜索
24 |
25 | Args:
26 | function_args: 工具参数
27 |
28 | Returns:
29 | Dict: 工具执行结果
30 | """
31 | try:
32 | query: str = function_args.get("query") # type: ignore
33 | # threshold = function_args.get("threshold", 0.4)
34 |
35 | # 检查LPMM知识库是否启用
36 | if qa_manager is None:
37 | logger.debug("LPMM知识库已禁用,跳过知识获取")
38 | return {"type": "info", "id": query, "content": "LPMM知识库已禁用"}
39 |
40 | # 调用知识库搜索
41 |
42 | knowledge_info = await qa_manager.get_knowledge(query)
43 |
44 | logger.debug(f"知识库查询结果: {knowledge_info}")
45 |
46 | if knowledge_info and knowledge_info.get("knowledge_items"):
47 | knowledge_parts = []
48 | for i, item in enumerate(knowledge_info["knowledge_items"]):
49 | knowledge_parts.append(f"- {item.get('content', 'N/A')}")
50 |
51 | knowledge_text = "\n".join(knowledge_parts)
52 | summary = knowledge_info.get("summary", "无总结")
53 | content = f"关于 '{query}', 你知道以下信息:\n{knowledge_text}\n\n总结: {summary}"
54 | else:
55 | content = f"关于 '{query}',你的知识库里好像没有相关的信息呢"
56 | return {"type": "lpmm_knowledge", "id": query, "content": content}
57 | except Exception as e:
58 | # 捕获异常并记录错误
59 | logger.error(f"知识库搜索工具执行失败: {e!s}")
60 | # 在其他异常情况下,确保 id 仍然是 query (如果它被定义了)
61 | query_id = query if "query" in locals() else "unknown_query"
62 | return {"type": "info", "id": query_id, "content": f"lpmm知识库搜索失败,炸了: {e!s}"}
63 |
--------------------------------------------------------------------------------
/src/plugins/built_in/maizone_refactored/__init__.py:
--------------------------------------------------------------------------------
1 | from src.plugin_system.base.plugin_metadata import PluginMetadata
2 |
3 | __plugin_meta__ = PluginMetadata(
4 | name="MaiZone(麦麦空间)- 重构版",
5 | description="(重构版)让你的麦麦发QQ空间说说、评论、点赞,支持AI配图、定时发送和自动监控功能",
6 | usage="该插件提供 `send_feed` 和 `read_feed` action,以及 `send_feed` command。",
7 | version="3.0.0",
8 | author="MoFox-Studio",
9 | license="GPL-v3.0",
10 | repository_url="https://github.com/MoFox-Studio",
11 | keywords=["QQ空间", "说说", "动态", "评论", "点赞", "自动化", "AI配图"],
12 | categories=["社交", "自动化", "QQ空间"],
13 | extra={
14 | "is_built_in": False,
15 | "plugin_type": "social",
16 | },
17 | )
18 |
--------------------------------------------------------------------------------
/src/plugins/built_in/maizone_refactored/actions/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoFox-Studio/MoFox_Bot/3f9b3509d3c7d6fa4ea3ae40fdcb4b53b3196450/src/plugins/built_in/maizone_refactored/actions/__init__.py
--------------------------------------------------------------------------------
/src/plugins/built_in/maizone_refactored/actions/send_feed_action.py:
--------------------------------------------------------------------------------
1 | """
2 | 发送说说动作组件
3 | """
4 |
5 | from src.common.logger import get_logger
6 | from src.plugin_system import ActionActivationType, BaseAction, ChatMode
7 | from src.plugin_system.apis import generator_api
8 | from src.plugin_system.apis.permission_api import permission_api
9 |
10 | from ..services.manager import get_qzone_service
11 |
12 | logger = get_logger("MaiZone.SendFeedAction")
13 |
14 |
15 | class SendFeedAction(BaseAction):
16 | """
17 | 当检测到用户意图是发送说说时,此动作被激活。
18 | """
19 |
20 | action_name: str = "send_feed"
21 | action_description: str = "发送一条关于特定主题的说说"
22 | activation_type: ActionActivationType = ActionActivationType.KEYWORD
23 | mode_enable: ChatMode = ChatMode.ALL
24 | activation_keywords: list = ["发说说", "发空间", "发动态"]
25 |
26 | action_parameters = {
27 | "topic": "用户想要发送的说说主题",
28 | "user_name": "请求你发说说的好友的昵称",
29 | }
30 |
31 | def __init__(self, *args, **kwargs):
32 | super().__init__(*args, **kwargs)
33 |
34 | async def _check_permission(self) -> bool:
35 | """检查当前用户是否有权限执行此动作"""
36 | platform = self.chat_stream.platform
37 | user_id = self.chat_stream.user_info.user_id
38 |
39 | # 使用权限API检查用户是否有发送说说的权限
40 | return await permission_api.check_permission(platform, user_id, "plugin.maizone.send_feed")
41 |
42 | async def execute(self) -> tuple[bool, str]:
43 | """
44 | 执行动作的核心逻辑。
45 | """
46 | if not await self._check_permission():
47 | _, reply_set, _ = await generator_api.generate_reply(
48 | chat_stream=self.chat_stream,
49 | action_data={"extra_info_block": "无权命令你发送说说,请用符合你人格特点的方式拒绝请求"},
50 | )
51 | if reply_set and isinstance(reply_set, list):
52 | for reply_type, reply_content in reply_set:
53 | if reply_type == "text":
54 | await self.send_text(reply_content)
55 | return False, "权限不足"
56 |
57 | topic = self.action_data.get("topic", "")
58 | stream_id = self.chat_stream.stream_id
59 |
60 | try:
61 | qzone_service = get_qzone_service()
62 | result = await qzone_service.send_feed(topic, stream_id)
63 |
64 | if result.get("success"):
65 | _, reply_set, _ = await generator_api.generate_reply(
66 | chat_stream=self.chat_stream,
67 | action_data={
68 | "extra_info_block": f"你刚刚成功发送了一条关于“{topic or '随机'}”的说说,内容是:{result.get('message', '')}"
69 | },
70 | )
71 | if reply_set and isinstance(reply_set, list):
72 | for reply_type, reply_content in reply_set:
73 | if reply_type == "text":
74 | await self.send_text(reply_content)
75 | else:
76 | await self.send_text("我发完说说啦,快去看看吧!")
77 | return True, "发送成功"
78 | else:
79 | await self.send_text(f"发送失败了呢,原因好像是:{result.get('message', '未知错误')}")
80 | return False, result.get("message", "未知错误")
81 |
82 | except Exception as e:
83 | logger.error(f"执行发送说说动作时发生未知异常: {e}", exc_info=True)
84 | await self.send_text("糟糕,发送的时候网络好像波动了一下...")
85 | return False, "动作执行异常"
86 |
--------------------------------------------------------------------------------
/src/plugins/built_in/maizone_refactored/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoFox-Studio/MoFox_Bot/3f9b3509d3c7d6fa4ea3ae40fdcb4b53b3196450/src/plugins/built_in/maizone_refactored/commands/__init__.py
--------------------------------------------------------------------------------
/src/plugins/built_in/maizone_refactored/commands/send_feed_command.py:
--------------------------------------------------------------------------------
1 | """
2 | 发送说说命令 await self.send_text(f"收到!正在为你生成关于"{topic or '随机'}"的说说,请稍候...【热重载测试成功】")件
3 | """
4 |
5 | from src.common.logger import get_logger
6 | from src.plugin_system.base.command_args import CommandArgs
7 | from src.plugin_system.base.plus_command import PlusCommand
8 | from src.plugin_system.utils.permission_decorators import require_permission
9 |
10 | from ..services.manager import get_config_getter, get_qzone_service
11 |
12 | logger = get_logger("MaiZone.SendFeedCommand")
13 |
14 |
15 | class SendFeedCommand(PlusCommand):
16 | """
17 | 响应用户通过 `/send_feed` 命令发送说说的请求。
18 | 测试热重载功能 - 这是一个测试注释,现在应该可以正常工作了!
19 | """
20 |
21 | command_name: str = "send_feed"
22 | command_description: str = "发一条QQ空间说说"
23 | command_aliases = ["发空间"]
24 |
25 | def __init__(self, *args, **kwargs):
26 | super().__init__(*args, **kwargs)
27 |
28 | @require_permission("plugin.maizone.send_feed")
29 | async def execute(self, args: CommandArgs) -> tuple[bool, str, bool]:
30 | """
31 | 执行命令的核心逻辑。
32 | """
33 |
34 | topic = args.get_remaining()
35 | stream_id = self.message.chat_stream.stream_id
36 |
37 | await self.send_text(f"收到!正在为你生成关于“{topic or '随机'}”的说说,请稍候...")
38 |
39 | try:
40 | qzone_service = get_qzone_service()
41 | result = await qzone_service.send_feed(topic, stream_id)
42 |
43 | if result.get("success"):
44 | reply_message = f"已经成功发送说说:\n{result.get('message', '')}"
45 | get_config = get_config_getter()
46 | if get_config("send.enable_reply", True):
47 | await self.send_text(reply_message)
48 | return True, "发送成功", True
49 | else:
50 | await self.send_text(f"哎呀,发送失败了:{result.get('message', '未知错误')}")
51 | return False, result.get("message", "未知错误"), True
52 |
53 | except Exception as e:
54 | logger.error(f"执行发送说说命令时发生未知异常: {e},它的类型是:{type(e)}", exc_info=True)
55 | await self.send_text("呜... 发送过程中好像出了点问题。")
56 | return False, "命令执行异常", True
57 |
--------------------------------------------------------------------------------
/src/plugins/built_in/maizone_refactored/services/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoFox-Studio/MoFox_Bot/3f9b3509d3c7d6fa4ea3ae40fdcb4b53b3196450/src/plugins/built_in/maizone_refactored/services/__init__.py
--------------------------------------------------------------------------------
/src/plugins/built_in/maizone_refactored/services/manager.py:
--------------------------------------------------------------------------------
1 | """
2 | 服务管理器/定位器
3 | 这是一个独立的模块,用于注册和获取插件内的全局服务实例,以避免循环导入。
4 | """
5 |
6 | from collections.abc import Callable
7 | from typing import Any
8 |
9 | from .qzone_service import QZoneService
10 |
11 | # --- 全局服务注册表 ---
12 | _services: dict[str, Any] = {}
13 |
14 |
15 | def register_service(name: str, instance: Any):
16 | """将一个服务实例注册到全局注册表。"""
17 | _services[name] = instance
18 |
19 |
20 | def get_qzone_service() -> QZoneService:
21 | """全局可用的QZone服务获取函数"""
22 | return _services["qzone"]
23 |
24 |
25 | def get_config_getter() -> Callable:
26 | """全局可用的配置获取函数"""
27 | return _services["get_config"]
28 |
--------------------------------------------------------------------------------
/src/plugins/built_in/maizone_refactored/services/monitor_service.py:
--------------------------------------------------------------------------------
1 | """
2 | 好友动态监控服务
3 | """
4 |
5 | import asyncio
6 | import traceback
7 | from collections.abc import Callable
8 |
9 | from src.common.logger import get_logger
10 |
11 | from .qzone_service import QZoneService
12 |
13 | logger = get_logger("MaiZone.MonitorService")
14 |
15 |
16 | class MonitorService:
17 | """好友动态监控服务"""
18 |
19 | def __init__(self, get_config: Callable, qzone_service: QZoneService):
20 | self.get_config = get_config
21 | self.qzone_service = qzone_service
22 | self.is_running = False
23 | self.task = None
24 |
25 | async def start(self):
26 | """启动监控任务"""
27 | if self.is_running:
28 | return
29 | self.is_running = True
30 | self.task = asyncio.create_task(self._monitor_loop())
31 | logger.info("好友动态监控任务已启动")
32 |
33 | async def stop(self):
34 | """停止监控任务"""
35 | if not self.is_running:
36 | return
37 | self.is_running = False
38 | if self.task:
39 | self.task.cancel()
40 | try:
41 | await self.task
42 | except asyncio.CancelledError:
43 | pass
44 | logger.info("好友动态监控任务已停止")
45 |
46 | async def _monitor_loop(self):
47 | """监控任务主循环"""
48 | # 插件启动后,延迟一段时间再开始第一次监控
49 | await asyncio.sleep(60)
50 |
51 | while self.is_running:
52 | try:
53 | if not self.get_config("monitor.enable_auto_monitor", False):
54 | await asyncio.sleep(60)
55 | continue
56 |
57 | interval_minutes = self.get_config("monitor.interval_minutes", 10)
58 |
59 | await self.qzone_service.monitor_feeds()
60 |
61 | logger.info(f"本轮监控完成,将在 {interval_minutes} 分钟后进行下一次检查。")
62 | await asyncio.sleep(interval_minutes * 60)
63 |
64 | except asyncio.CancelledError:
65 | break
66 | except Exception as e:
67 | logger.error(f"监控任务循环出错: {e}\n{traceback.format_exc()}")
68 | await asyncio.sleep(300)
69 |
--------------------------------------------------------------------------------
/src/plugins/built_in/maizone_refactored/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoFox-Studio/MoFox_Bot/3f9b3509d3c7d6fa4ea3ae40fdcb4b53b3196450/src/plugins/built_in/maizone_refactored/utils/__init__.py
--------------------------------------------------------------------------------
/src/plugins/built_in/napcat_adapter_plugin/__init__.py:
--------------------------------------------------------------------------------
1 | from src.plugin_system.base.plugin_metadata import PluginMetadata
2 |
3 | __plugin_meta__ = PluginMetadata(
4 | name="napcat_plugin",
5 | description="基于OneBot 11协议的NapCat QQ协议插件,提供完整的QQ机器人API接口,使用现有adapter连接",
6 | usage="该插件提供 `napcat_tool` tool。",
7 | version="1.0.0",
8 | author="Windpicker_owo",
9 | license="GPL-v3.0-or-later",
10 | repository_url="https://github.com/Windpicker-owo/InternetSearchPlugin",
11 | keywords=["qq", "bot", "napcat", "onebot", "api", "websocket"],
12 | categories=["protocol"],
13 | extra={
14 | "is_built_in": False,
15 | },
16 | )
17 |
--------------------------------------------------------------------------------
/src/plugins/built_in/napcat_adapter_plugin/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "MaiBotNapcatAdapter"
3 | version = "0.4.8"
4 | description = "A MaiBot adapter for Napcat"
5 | dependencies = [
6 | "ruff>=0.12.9",
7 | ]
8 |
9 | [tool.ruff]
10 |
11 | include = ["*.py"]
12 |
13 | # 行长度设置
14 | line-length = 120
15 |
16 | [tool.ruff.lint]
17 | fixable = ["ALL"]
18 | unfixable = []
19 |
20 | # 启用的规则
21 | select = [
22 | "E", # pycodestyle 错误
23 | "F", # pyflakes
24 | "B", # flake8-bugbear
25 | ]
26 |
27 | ignore = ["E711","E501"]
28 |
29 | [tool.ruff.format]
30 | docstring-code-format = true
31 | indent-style = "space"
32 |
33 |
34 | # 使用双引号表示字符串
35 | quote-style = "double"
36 |
37 | # 尊重魔法尾随逗号
38 | # 例如:
39 | # items = [
40 | # "apple",
41 | # "banana",
42 | # "cherry",
43 | # ]
44 | skip-magic-trailing-comma = false
45 |
46 | # 自动检测合适的换行符
47 | line-ending = "auto"
48 |
--------------------------------------------------------------------------------
/src/plugins/built_in/napcat_adapter_plugin/src/__init__.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | import tomlkit
3 | import os
4 | from src.common.logger import get_logger
5 |
6 | logger = get_logger("napcat_adapter")
7 |
8 |
9 | class CommandType(Enum):
10 | """命令类型"""
11 |
12 | GROUP_BAN = "set_group_ban" # 禁言用户
13 | GROUP_WHOLE_BAN = "set_group_whole_ban" # 群全体禁言
14 | GROUP_KICK = "set_group_kick" # 踢出群聊
15 | SEND_POKE = "send_poke" # 戳一戳
16 | DELETE_MSG = "delete_msg" # 撤回消息
17 | AI_VOICE_SEND = "send_group_ai_record" # 发送群AI语音
18 | SET_EMOJI_LIKE = "set_msg_emoji_like" # 设置表情回应
19 | SEND_AT_MESSAGE = "send_at_message" # 艾特用户并发送消息
20 | SEND_LIKE = "send_like" # 点赞
21 |
22 | def __str__(self) -> str:
23 | return self.value
24 |
25 |
26 | pyproject_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "pyproject.toml")
27 | toml_data = tomlkit.parse(open(pyproject_path, "r", encoding="utf-8").read())
28 | project_data = toml_data.get("project", {})
29 | version = project_data.get("version", "unknown")
30 | logger.info(f"版本\n\nMaiBot-Napcat-Adapter 版本: {version}\n喜欢的话点个star喵~\n")
31 |
--------------------------------------------------------------------------------
/src/plugins/built_in/napcat_adapter_plugin/src/mmc_com_layer.py:
--------------------------------------------------------------------------------
1 | from maim_message import Router, RouteConfig, TargetConfig
2 | from src.common.logger import get_logger
3 | import os
4 | from .send_handler import send_handler
5 | from src.plugin_system.apis import config_api
6 |
7 | logger = get_logger("napcat_adapter")
8 |
9 | router = None
10 |
11 |
12 | def create_router(plugin_config: dict):
13 | """创建路由器实例"""
14 | global router
15 | platform_name = config_api.get_plugin_config(plugin_config, "maibot_server.platform_name", "qq")
16 | host = os.getenv("HOST", "127.0.0.1")
17 | port = os.getenv("PORT", "8000")
18 | logger.debug(f"初始化MaiBot连接,使用地址:{host}:{port}")
19 | route_config = RouteConfig(
20 | route_config={
21 | platform_name: TargetConfig(
22 | url=f"ws://{host}:{port}/ws",
23 | token=None,
24 | )
25 | }
26 | )
27 | router = Router(route_config)
28 | return router
29 |
30 |
31 | async def mmc_start_com(plugin_config: dict | None = None):
32 | """启动MaiBot连接"""
33 | logger.debug("正在连接MaiBot")
34 | if plugin_config:
35 | create_router(plugin_config)
36 |
37 | if router:
38 | router.register_class_handler(send_handler.handle_message)
39 | await router.run()
40 |
41 |
42 | async def mmc_stop_com():
43 | """停止MaiBot连接"""
44 | if router:
45 | await router.stop()
46 |
--------------------------------------------------------------------------------
/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/__init__.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 |
4 | class MetaEventType:
5 | lifecycle = "lifecycle" # 生命周期
6 |
7 | class Lifecycle:
8 | connect = "connect" # 生命周期 - WebSocket 连接成功
9 |
10 | heartbeat = "heartbeat" # 心跳
11 |
12 |
13 | class MessageType: # 接受消息大类
14 | private = "private" # 私聊消息
15 |
16 | class Private:
17 | friend = "friend" # 私聊消息 - 好友
18 | group = "group" # 私聊消息 - 群临时
19 | group_self = "group_self" # 私聊消息 - 群中自身发送
20 | other = "other" # 私聊消息 - 其他
21 |
22 | group = "group" # 群聊消息
23 |
24 | class Group:
25 | normal = "normal" # 群聊消息 - 普通
26 | anonymous = "anonymous" # 群聊消息 - 匿名消息
27 | notice = "notice" # 群聊消息 - 系统提示
28 |
29 |
30 | class NoticeType: # 通知事件
31 | friend_recall = "friend_recall" # 私聊消息撤回
32 | group_recall = "group_recall" # 群聊消息撤回
33 | notify = "notify"
34 | group_ban = "group_ban" # 群禁言
35 | group_msg_emoji_like = "group_msg_emoji_like" # 群聊表情回复
36 |
37 | class Notify:
38 | poke = "poke" # 戳一戳
39 | input_status = "input_status" # 正在输入
40 |
41 | class GroupBan:
42 | ban = "ban" # 禁言
43 | lift_ban = "lift_ban" # 解除禁言
44 |
45 |
46 | class RealMessageType: # 实际消息分类
47 | text = "text" # 纯文本
48 | face = "face" # qq表情
49 | image = "image" # 图片
50 | record = "record" # 语音
51 | video = "video" # 视频
52 | at = "at" # @某人
53 | rps = "rps" # 猜拳魔法表情
54 | dice = "dice" # 骰子
55 | shake = "shake" # 私聊窗口抖动(只收)
56 | poke = "poke" # 群聊戳一戳
57 | share = "share" # 链接分享(json形式)
58 | reply = "reply" # 回复消息
59 | forward = "forward" # 转发消息
60 | node = "node" # 转发消息节点
61 | json = "json" # json消息
62 |
63 |
64 | class MessageSentType:
65 | private = "private"
66 |
67 | class Private:
68 | friend = "friend"
69 | group = "group"
70 |
71 | group = "group"
72 |
73 | class Group:
74 | normal = "normal"
75 |
76 |
77 | class CommandType(Enum):
78 | """命令类型"""
79 |
80 | GROUP_BAN = "set_group_ban" # 禁言用户
81 | GROUP_WHOLE_BAN = "set_group_whole_ban" # 群全体禁言
82 | GROUP_KICK = "set_group_kick" # 踢出群聊
83 | SEND_POKE = "send_poke" # 戳一戳
84 | DELETE_MSG = "delete_msg" # 撤回消息
85 |
86 | def __str__(self) -> str:
87 | return self.value
88 |
89 |
90 | ACCEPT_FORMAT = ["text", "image", "emoji", "reply", "voice", "command", "voiceurl", "music", "videourl", "file"]
91 |
--------------------------------------------------------------------------------
/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/meta_event_handler.py:
--------------------------------------------------------------------------------
1 | from src.common.logger import get_logger
2 |
3 | logger = get_logger("napcat_adapter")
4 | from src.plugin_system.apis import config_api
5 | import time
6 | import asyncio
7 |
8 | from . import MetaEventType
9 |
10 |
11 | class MetaEventHandler:
12 | """
13 | 处理Meta事件
14 | """
15 |
16 | def __init__(self):
17 | self.interval = 5.0 # 默认值,稍后通过set_plugin_config设置
18 | self._interval_checking = False
19 | self.plugin_config = None
20 |
21 | def set_plugin_config(self, plugin_config: dict):
22 | """设置插件配置"""
23 | self.plugin_config = plugin_config
24 | # 更新interval值
25 | self.interval = (
26 | config_api.get_plugin_config(self.plugin_config, "napcat_server.heartbeat_interval", 5000) / 1000
27 | )
28 |
29 | async def handle_meta_event(self, message: dict) -> None:
30 | event_type = message.get("meta_event_type")
31 | if event_type == MetaEventType.lifecycle:
32 | sub_type = message.get("sub_type")
33 | if sub_type == MetaEventType.Lifecycle.connect:
34 | self_id = message.get("self_id")
35 | self.last_heart_beat = time.time()
36 | logger.info(f"Bot {self_id} 连接成功")
37 | asyncio.create_task(self.check_heartbeat(self_id))
38 | elif event_type == MetaEventType.heartbeat:
39 | if message["status"].get("online") and message["status"].get("good"):
40 | if not self._interval_checking:
41 | asyncio.create_task(self.check_heartbeat())
42 | self.last_heart_beat = time.time()
43 | self.interval = message.get("interval") / 1000
44 | else:
45 | self_id = message.get("self_id")
46 | logger.warning(f"Bot {self_id} Napcat 端异常!")
47 |
48 | async def check_heartbeat(self, id: int) -> None:
49 | self._interval_checking = True
50 | while True:
51 | now_time = time.time()
52 | if now_time - self.last_heart_beat > self.interval * 2:
53 | logger.error(f"Bot {id} 可能发生了连接断开,被下线,或者Napcat卡死!")
54 | break
55 | else:
56 | logger.debug("心跳正常")
57 | await asyncio.sleep(self.interval)
58 |
59 |
60 | meta_event_handler = MetaEventHandler()
61 |
--------------------------------------------------------------------------------
/src/plugins/built_in/napcat_adapter_plugin/src/response_pool.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import time
3 | from typing import Dict
4 | from src.common.logger import get_logger
5 | from src.plugin_system.apis import config_api
6 |
7 | logger = get_logger("napcat_adapter")
8 |
9 | response_dict: Dict = {}
10 | response_time_dict: Dict = {}
11 | plugin_config = None
12 |
13 |
14 | def set_plugin_config(config: dict):
15 | """设置插件配置"""
16 | global plugin_config
17 | plugin_config = config
18 |
19 |
20 | async def get_response(request_id: str, timeout: int = 10) -> dict:
21 | response = await asyncio.wait_for(_get_response(request_id), timeout)
22 | _ = response_time_dict.pop(request_id)
23 | logger.debug(f"响应信息id: {request_id} 已从响应字典中取出")
24 | return response
25 |
26 |
27 | async def _get_response(request_id: str) -> dict:
28 | """
29 | 内部使用的获取响应函数,主要用于在需要时获取响应
30 | """
31 | while request_id not in response_dict:
32 | await asyncio.sleep(0.2)
33 | return response_dict.pop(request_id)
34 |
35 |
36 | async def put_response(response: dict):
37 | echo_id = response.get("echo")
38 | now_time = time.time()
39 | response_dict[echo_id] = response
40 | response_time_dict[echo_id] = now_time
41 | logger.debug(f"响应信息id: {echo_id} 已存入响应字典")
42 |
43 |
44 | async def check_timeout_response() -> None:
45 | while True:
46 | cleaned_message_count: int = 0
47 | now_time = time.time()
48 |
49 | # 获取心跳间隔配置
50 | heartbeat_interval = 30 # 默认值
51 | if plugin_config:
52 | heartbeat_interval = config_api.get_plugin_config(plugin_config, "napcat_server.heartbeat_interval", 30)
53 |
54 | for echo_id, response_time in list(response_time_dict.items()):
55 | if now_time - response_time > heartbeat_interval:
56 | cleaned_message_count += 1
57 | response_dict.pop(echo_id)
58 | response_time_dict.pop(echo_id)
59 | logger.warning(f"响应消息 {echo_id} 超时,已删除")
60 | if cleaned_message_count > 0:
61 | logger.info(f"已删除 {cleaned_message_count} 条超时响应消息")
62 | await asyncio.sleep(heartbeat_interval)
63 |
--------------------------------------------------------------------------------
/src/plugins/built_in/napcat_adapter_plugin/todo.md:
--------------------------------------------------------------------------------
1 | # TODO List:
2 |
3 | - [x] logger使用主程序的
4 | - [ ] 使用插件系统的config系统
5 | - [x] 接收从napcat传递的所有信息
6 | - [ ] 优化架构,各模块解耦,暴露关键方法用于提供接口
7 | - [ ] 单独一个模块负责与主程序通信
8 | - [ ] 使用event系统完善接口api
9 |
10 |
11 | ---
12 | Event分为两种,一种是对外输出的event,由napcat插件自主触发并传递参数,另一种是接收外界输入的event,由外部插件触发并向napcat传递参数
13 |
14 |
15 | ## 例如,
16 |
17 | ### 对外输出的event:
18 |
19 | napcat_on_received_text -> (message_seg: Seg) 接受到qq的文字消息,会向handler传递一个Seg
20 | napcat_on_received_face -> (message_seg: Seg) 接受到qq的表情消息,会向handler传递一个Seg
21 | napcat_on_received_reply -> (message_seg: Seg) 接受到qq的回复消息,会向handler传递一个Seg
22 | napcat_on_received_image -> (message_seg: Seg) 接受到qq的图片消息,会向handler传递一个Seg
23 | napcat_on_received_image -> (message_seg: Seg) 接受到qq的图片消息,会向handler传递一个Seg
24 | napcat_on_received_record -> (message_seg: Seg) 接受到qq的语音消息,会向handler传递一个Seg
25 | napcat_on_received_rps -> (message_seg: Seg) 接受到qq的猜拳魔法表情,会向handler传递一个Seg
26 | napcat_on_received_friend_invitation -> (user_id: str) 接受到qq的好友请求,会向handler传递一个user_id
27 | ...
28 |
29 | 此类event不接受外部插件的触发,只能由napcat插件统一触发。
30 |
31 | 外部插件需要编写handler并订阅此类事件。
32 | ```python
33 | from src.plugin_system.core.event_manager import event_manager
34 | from src.plugin_system.base.base_event import HandlerResult
35 |
36 | class MyEventHandler(BaseEventHandler):
37 | handler_name = "my_handler"
38 | handler_description = "我的自定义事件处理器"
39 | weight = 10 # 权重,越大越先执行
40 | intercept_message = False # 是否拦截消息
41 | init_subscribe = ["napcat_on_received_text"] # 初始订阅的事件
42 |
43 | async def execute(self, params: dict) -> HandlerResult:
44 | """处理事件"""
45 | try:
46 | message = params.get("message_seg")
47 | print(f"收到消息: {message.data}")
48 |
49 | # 业务逻辑处理
50 | # ...
51 |
52 | return HandlerResult(
53 | success=True,
54 | continue_process=True, # 是否继续让其他处理器处理
55 | message="处理成功",
56 | handler_name=self.handler_name
57 | )
58 | except Exception as e:
59 | return HandlerResult(
60 | success=False,
61 | continue_process=True,
62 | message=f"处理失败: {str(e)}",
63 | handler_name=self.handler_name
64 | )
65 |
66 | ```
67 |
68 | ### 接收外界输入的event:
69 |
70 | napcat_kick_group <- (user_id, group_id) 踢出某个群组中的某个用户
71 | napcat_mute_user <- (user_id, group_id, time) 禁言某个群组中的某个用户
72 | napcat_unmute_user <- (user_id, group_id) 取消禁言某个群组中的某个用户
73 | napcat_mute_group <- (user_id, group_id) 禁言某个群组
74 | napcat_unmute_group <- (user_id, group_id) 取消禁言某个群组
75 | napcat_add_friend <- (user_id) 向某个用户发出好友请求
76 | napcat_accept_friend <- (user_id) 接收某个用户的好友请求
77 | napcat_reject_friend <- (user_id) 拒绝某个用户的好友请求
78 | ...
79 | 此类事件只由外部插件触发并传递参数,由napcat完成请求任务。
80 |
81 | 外部插件需要触发此类的event并传递正确的参数。
82 |
83 | ```python
84 | from src.plugin_system.core.event_manager import event_manager
85 |
86 | # 触发事件
87 | await event_manager.trigger_event("napcat_accept_friend", user_id = 1234123)
88 | ```
89 |
90 |
--------------------------------------------------------------------------------
/src/plugins/built_in/permission_management/__init__.py:
--------------------------------------------------------------------------------
1 | from src.plugin_system.base.plugin_metadata import PluginMetadata
2 |
3 | __plugin_meta__ = PluginMetadata(
4 | name="权限管理插件(Permission Management)",
5 | description="通过系统API管理权限",
6 | usage="该插件提供 `permission_management` command。",
7 | version="1.0.0",
8 | author="MoFox-Studio",
9 | license="GPL-v3.0-or-later",
10 | repository_url="https://github.com/MoFox-Studio",
11 | keywords=["plugins", "permission", "management", "built-in"],
12 | extra={
13 | "is_built_in": True,
14 | "plugin_type": "permission",
15 | },
16 | )
17 |
--------------------------------------------------------------------------------
/src/plugins/built_in/plugin_management/__init__.py:
--------------------------------------------------------------------------------
1 | from src.plugin_system.base.plugin_metadata import PluginMetadata
2 |
3 | __plugin_meta__ = PluginMetadata(
4 | name="插件和组件管理 (Plugin and Component Management)",
5 | description="通过系统API管理插件和组件的生命周期,包括加载、卸载、启用和禁用等操作。",
6 | usage="该插件提供 `plugin_management` command。",
7 | version="1.0.0",
8 | author="MaiBot团队",
9 | license="GPL-v3.0-or-later",
10 | repository_url="https://github.com/MaiM-with-u/maibot",
11 | keywords=["plugins", "components", "management", "built-in"],
12 | categories=["Core System", "Plugin Management"],
13 | extra={
14 | "is_built_in": True,
15 | "plugin_type": "plugin_management",
16 | },
17 | )
18 |
--------------------------------------------------------------------------------
/src/plugins/built_in/proactive_thinker/__init__.py:
--------------------------------------------------------------------------------
1 | from src.plugin_system.base.plugin_metadata import PluginMetadata
2 |
3 | __plugin_meta__ = PluginMetadata(
4 | name="MoFox-Bot主动思考",
5 | description="主动思考插件",
6 | usage="该插件由系统自动触发。",
7 | version="1.0.0",
8 | author="MoFox-Studio",
9 | license="GPL-v3.0-or-later",
10 | repository_url="https://github.com/MoFox-Studio",
11 | keywords=["主动思考", "自己发消息"],
12 | categories=["Chat", "Integration"],
13 | extra={"is_built_in": True, "plugin_type": "functional"},
14 | )
15 |
--------------------------------------------------------------------------------
/src/plugins/built_in/proactive_thinker/plugin.py:
--------------------------------------------------------------------------------
1 | from src.common.logger import get_logger
2 | from src.plugin_system import (
3 | BaseEventHandler,
4 | BasePlugin,
5 | ConfigField,
6 | EventHandlerInfo,
7 | register_plugin,
8 | )
9 | from src.plugin_system.base.base_plugin import BasePlugin
10 |
11 | from .proacive_thinker_event import ProactiveThinkerEventHandler
12 |
13 | logger = get_logger(__name__)
14 |
15 |
16 | @register_plugin
17 | class ProactiveThinkerPlugin(BasePlugin):
18 | """一个主动思考的插件,但现在还只是个空壳子"""
19 |
20 | plugin_name: str = "proactive_thinker"
21 | enable_plugin: bool = True
22 | dependencies: list[str] = []
23 | python_dependencies: list[str] = []
24 | config_file_name: str = "config.toml"
25 | config_schema: dict = {
26 | "plugin": {
27 | "config_version": ConfigField(type=str, default="1.1.0", description="配置文件版本"),
28 | },
29 | }
30 |
31 | def __init__(self, *args, **kwargs):
32 | super().__init__(*args, **kwargs)
33 |
34 | def get_plugin_components(self) -> list[tuple[EventHandlerInfo, type[BaseEventHandler]]]:
35 | """返回插件的EventHandler组件"""
36 | components: list[tuple[EventHandlerInfo, type[BaseEventHandler]]] = [
37 | (ProactiveThinkerEventHandler.get_handler_info(), ProactiveThinkerEventHandler)
38 | ]
39 | return components
40 |
--------------------------------------------------------------------------------
/src/plugins/built_in/social_toolkit_plugin/__init__.py:
--------------------------------------------------------------------------------
1 | from src.plugin_system.base.plugin_metadata import PluginMetadata
2 |
3 | __plugin_meta__ = PluginMetadata(
4 | name="MoFox-Bot工具箱",
5 | description="一个集合多种实用功能的插件,旨在提升聊天体验和效率。",
6 | usage="该插件提供多种命令,详情请查阅文档。",
7 | version="1.0.0",
8 | author="MoFox-Studio",
9 | license="GPL-v3.0-or-later",
10 | repository_url="https://github.com/MoFox-Studio",
11 | keywords=["emoji", "reaction", "like", "表情", "回应", "点赞"],
12 | categories=["Chat", "Integration"],
13 | extra={"is_built_in": "true", "plugin_type": "functional"},
14 | )
15 |
--------------------------------------------------------------------------------
/src/plugins/built_in/tts_plugin/__init__.py:
--------------------------------------------------------------------------------
1 | from src.plugin_system.base.plugin_metadata import PluginMetadata
2 |
3 | __plugin_meta__ = PluginMetadata(
4 | name="文本转语音插件 (Text-to-Speech)",
5 | description="将文本转换为语音进行播放的插件,支持多种语音模式和智能语音输出场景判断。",
6 | usage="该插件提供 `tts_action` action。",
7 | version="0.1.0",
8 | author="MaiBot团队",
9 | license="GPL-v3.0-or-later",
10 | repository_url="https://github.com/MaiM-with-u/maibot",
11 | keywords=["tts", "voice", "audio", "speech", "accessibility"],
12 | categories=["Audio Tools", "Accessibility", "Voice Assistant"],
13 | extra={
14 | "is_built_in": True,
15 | "plugin_type": "audio_processor",
16 | },
17 | )
18 |
--------------------------------------------------------------------------------
/src/plugins/built_in/web_search_tool/__init__.py:
--------------------------------------------------------------------------------
1 | from src.plugin_system.base.plugin_metadata import PluginMetadata
2 |
3 | __plugin_meta__ = PluginMetadata(
4 | name="Web Search Tool",
5 | description="A tool for searching the web.",
6 | usage="This plugin provides a `web_search` tool.",
7 | version="1.0.0",
8 | author="MoFox-Studio",
9 | license="GPL-v3.0-or-later",
10 | repository_url="https://github.com/MoFox-Studio",
11 | keywords=["web", "search", "tool"],
12 | categories=["Tools"],
13 | extra={
14 | "is_built_in": True,
15 | },
16 | )
17 |
--------------------------------------------------------------------------------
/src/plugins/built_in/web_search_tool/engines/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Search engines package
3 | """
4 |
--------------------------------------------------------------------------------
/src/plugins/built_in/web_search_tool/engines/base.py:
--------------------------------------------------------------------------------
1 | """
2 | Base search engine interface
3 | """
4 |
5 | from abc import ABC, abstractmethod
6 | from typing import Any
7 |
8 |
9 | class BaseSearchEngine(ABC):
10 | """
11 | 搜索引擎基类
12 | """
13 |
14 | @abstractmethod
15 | async def search(self, args: dict[str, Any]) -> list[dict[str, Any]]:
16 | """
17 | 执行搜索
18 |
19 | Args:
20 | args: 搜索参数,包含 query、num_results、time_range 等
21 |
22 | Returns:
23 | 搜索结果列表,每个结果包含 title、url、snippet、provider 字段
24 | """
25 | pass
26 |
27 | @abstractmethod
28 | def is_available(self) -> bool:
29 | """
30 | 检查搜索引擎是否可用
31 | """
32 | pass
33 |
--------------------------------------------------------------------------------
/src/plugins/built_in/web_search_tool/engines/ddg_engine.py:
--------------------------------------------------------------------------------
1 | """
2 | DuckDuckGo search engine implementation
3 | """
4 |
5 | from typing import Any
6 |
7 | from asyncddgs import aDDGS
8 |
9 | from src.common.logger import get_logger
10 |
11 | from .base import BaseSearchEngine
12 |
13 | logger = get_logger("ddg_engine")
14 |
15 |
16 | class DDGSearchEngine(BaseSearchEngine):
17 | """
18 | DuckDuckGo搜索引擎实现
19 | """
20 |
21 | def is_available(self) -> bool:
22 | """检查DuckDuckGo搜索引擎是否可用"""
23 | return True # DuckDuckGo不需要API密钥,总是可用
24 |
25 | async def search(self, args: dict[str, Any]) -> list[dict[str, Any]]:
26 | """执行DuckDuckGo搜索"""
27 | query = args["query"]
28 | num_results = args.get("num_results", 3)
29 |
30 | try:
31 | async with aDDGS() as ddgs:
32 | search_response = await ddgs.text(query, max_results=num_results)
33 |
34 | return [
35 | {"title": r.get("title"), "url": r.get("href"), "snippet": r.get("body"), "provider": "DuckDuckGo"}
36 | for r in search_response
37 | ]
38 | except Exception as e:
39 | logger.error(f"DuckDuckGo 搜索失败: {e}")
40 | return []
41 |
--------------------------------------------------------------------------------
/src/plugins/built_in/web_search_tool/engines/exa_engine.py:
--------------------------------------------------------------------------------
1 | """
2 | Exa search engine implementation
3 | """
4 |
5 | import asyncio
6 | import functools
7 | from datetime import datetime, timedelta
8 | from typing import Any
9 |
10 | from exa_py import Exa
11 |
12 | from src.common.logger import get_logger
13 | from src.plugin_system.apis import config_api
14 |
15 | from ..utils.api_key_manager import create_api_key_manager_from_config
16 | from .base import BaseSearchEngine
17 |
18 | logger = get_logger("exa_engine")
19 |
20 |
21 | class ExaSearchEngine(BaseSearchEngine):
22 | """
23 | Exa搜索引擎实现
24 | """
25 |
26 | def __init__(self):
27 | self._initialize_clients()
28 |
29 | def _initialize_clients(self):
30 | """初始化Exa客户端"""
31 | # 从主配置文件读取API密钥
32 | exa_api_keys = config_api.get_global_config("web_search.exa_api_keys", None)
33 |
34 | # 创建API密钥管理器
35 | self.api_manager = create_api_key_manager_from_config(exa_api_keys, lambda key: Exa(api_key=key), "Exa")
36 |
37 | def is_available(self) -> bool:
38 | """检查Exa搜索引擎是否可用"""
39 | return self.api_manager.is_available()
40 |
41 | async def search(self, args: dict[str, Any]) -> list[dict[str, Any]]:
42 | """执行Exa搜索"""
43 | if not self.is_available():
44 | return []
45 |
46 | query = args["query"]
47 | num_results = args.get("num_results", 3)
48 | time_range = args.get("time_range", "any")
49 |
50 | exa_args = {"num_results": num_results, "text": True, "highlights": True}
51 | if time_range != "any":
52 | today = datetime.now()
53 | start_date = today - timedelta(days=7 if time_range == "week" else 30)
54 | exa_args["start_published_date"] = start_date.strftime("%Y-%m-%d")
55 |
56 | try:
57 | # 使用API密钥管理器获取下一个客户端
58 | exa_client = self.api_manager.get_next_client()
59 | if not exa_client:
60 | logger.error("无法获取Exa客户端")
61 | return []
62 |
63 | loop = asyncio.get_running_loop()
64 | func = functools.partial(exa_client.search_and_contents, query, **exa_args)
65 | search_response = await loop.run_in_executor(None, func)
66 |
67 | return [
68 | {
69 | "title": res.title,
70 | "url": res.url,
71 | "snippet": " ".join(getattr(res, "highlights", [])) or (getattr(res, "text", "")[:250] + "..."),
72 | "provider": "Exa",
73 | }
74 | for res in search_response.results
75 | ]
76 | except Exception as e:
77 | logger.error(f"Exa 搜索失败: {e}")
78 | return []
79 |
--------------------------------------------------------------------------------
/src/plugins/built_in/web_search_tool/engines/tavily_engine.py:
--------------------------------------------------------------------------------
1 | """
2 | Tavily search engine implementation
3 | """
4 |
5 | import asyncio
6 | import functools
7 | from typing import Any
8 |
9 | from tavily import TavilyClient
10 |
11 | from src.common.logger import get_logger
12 | from src.plugin_system.apis import config_api
13 |
14 | from ..utils.api_key_manager import create_api_key_manager_from_config
15 | from .base import BaseSearchEngine
16 |
17 | logger = get_logger("tavily_engine")
18 |
19 |
20 | class TavilySearchEngine(BaseSearchEngine):
21 | """
22 | Tavily搜索引擎实现
23 | """
24 |
25 | def __init__(self):
26 | self._initialize_clients()
27 |
28 | def _initialize_clients(self):
29 | """初始化Tavily客户端"""
30 | # 从主配置文件读取API密钥
31 | tavily_api_keys = config_api.get_global_config("web_search.tavily_api_keys", None)
32 |
33 | # 创建API密钥管理器
34 | self.api_manager = create_api_key_manager_from_config(
35 | tavily_api_keys, lambda key: TavilyClient(api_key=key), "Tavily"
36 | )
37 |
38 | def is_available(self) -> bool:
39 | """检查Tavily搜索引擎是否可用"""
40 | return self.api_manager.is_available()
41 |
42 | async def search(self, args: dict[str, Any]) -> list[dict[str, Any]]:
43 | """执行Tavily搜索"""
44 | if not self.is_available():
45 | return []
46 |
47 | query = args["query"]
48 | num_results = args.get("num_results", 3)
49 | time_range = args.get("time_range", "any")
50 |
51 | try:
52 | # 使用API密钥管理器获取下一个客户端
53 | tavily_client = self.api_manager.get_next_client()
54 | if not tavily_client:
55 | logger.error("无法获取Tavily客户端")
56 | return []
57 |
58 | # 构建Tavily搜索参数
59 | search_params = {
60 | "query": query,
61 | "max_results": num_results,
62 | "search_depth": "basic",
63 | "include_answer": False,
64 | "include_raw_content": False,
65 | }
66 |
67 | # 根据时间范围调整搜索参数
68 | if time_range == "week":
69 | search_params["days"] = 7
70 | elif time_range == "month":
71 | search_params["days"] = 30
72 |
73 | loop = asyncio.get_running_loop()
74 | func = functools.partial(tavily_client.search, **search_params)
75 | search_response = await loop.run_in_executor(None, func)
76 |
77 | results = []
78 | if search_response and "results" in search_response:
79 | for res in search_response["results"]:
80 | results.append(
81 | {
82 | "title": res.get("title", "无标题"),
83 | "url": res.get("url", ""),
84 | "snippet": res.get("content", "")[:300] + "..." if res.get("content") else "无摘要",
85 | "provider": "Tavily",
86 | }
87 | )
88 |
89 | return results
90 |
91 | except Exception as e:
92 | logger.error(f"Tavily 搜索失败: {e}")
93 | return []
94 |
--------------------------------------------------------------------------------
/src/plugins/built_in/web_search_tool/tools/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Tools package
3 | """
4 |
--------------------------------------------------------------------------------
/src/plugins/built_in/web_search_tool/utils/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Web search tool utilities package
3 | """
4 |
--------------------------------------------------------------------------------
/src/plugins/built_in/web_search_tool/utils/api_key_manager.py:
--------------------------------------------------------------------------------
1 | """
2 | API密钥管理器,提供轮询机制
3 | """
4 |
5 | import itertools
6 | from collections.abc import Callable
7 | from typing import Generic, TypeVar
8 |
9 | from src.common.logger import get_logger
10 |
11 | logger = get_logger("api_key_manager")
12 |
13 | T = TypeVar("T")
14 |
15 |
16 | class APIKeyManager(Generic[T]):
17 | """
18 | API密钥管理器,支持轮询机制
19 | """
20 |
21 | def __init__(self, api_keys: list[str], client_factory: Callable[[str], T], service_name: str = "Unknown"):
22 | """
23 | 初始化API密钥管理器
24 |
25 | Args:
26 | api_keys: API密钥列表
27 | client_factory: 客户端工厂函数,接受API密钥参数并返回客户端实例
28 | service_name: 服务名称,用于日志记录
29 | """
30 | self.service_name = service_name
31 | self.clients: list[T] = []
32 | self.client_cycle: itertools.cycle | None = None
33 |
34 | if api_keys:
35 | # 过滤有效的API密钥,排除None、空字符串、"None"字符串等
36 | valid_keys = []
37 | for key in api_keys:
38 | if isinstance(key, str) and key.strip() and key.strip().lower() not in ("none", "null", ""):
39 | valid_keys.append(key.strip())
40 |
41 | if valid_keys:
42 | try:
43 | self.clients = [client_factory(key) for key in valid_keys]
44 | self.client_cycle = itertools.cycle(self.clients)
45 | logger.info(f"🔑 {service_name} 成功加载 {len(valid_keys)} 个 API 密钥")
46 | except Exception as e:
47 | logger.error(f"❌ 初始化 {service_name} 客户端失败: {e}")
48 | self.clients = []
49 | self.client_cycle = None
50 | else:
51 | logger.warning(f"⚠️ {service_name} API Keys 配置无效(包含None或空值),{service_name} 功能将不可用")
52 | else:
53 | logger.warning(f"⚠️ {service_name} API Keys 未配置,{service_name} 功能将不可用")
54 |
55 | def is_available(self) -> bool:
56 | """检查是否有可用的客户端"""
57 | return bool(self.clients and self.client_cycle)
58 |
59 | def get_next_client(self) -> T | None:
60 | """获取下一个客户端(轮询)"""
61 | if not self.is_available():
62 | return None
63 | return next(self.client_cycle)
64 |
65 | def get_client_count(self) -> int:
66 | """获取可用客户端数量"""
67 | return len(self.clients)
68 |
69 |
70 | def create_api_key_manager_from_config(
71 | config_keys: list[str] | None, client_factory: Callable[[str], T], service_name: str
72 | ) -> APIKeyManager[T]:
73 | """
74 | 从配置创建API密钥管理器的便捷函数
75 |
76 | Args:
77 | config_keys: 从配置读取的API密钥列表
78 | client_factory: 客户端工厂函数
79 | service_name: 服务名称
80 |
81 | Returns:
82 | API密钥管理器实例
83 | """
84 | api_keys = config_keys if isinstance(config_keys, list) else []
85 | return APIKeyManager(api_keys, client_factory, service_name)
86 |
--------------------------------------------------------------------------------
/src/plugins/built_in/web_search_tool/utils/formatters.py:
--------------------------------------------------------------------------------
1 | """
2 | Formatters for web search results
3 | """
4 |
5 | from typing import Any
6 |
7 |
8 | def format_search_results(results: list[dict[str, Any]]) -> str:
9 | """
10 | 格式化搜索结果为字符串
11 | """
12 | if not results:
13 | return "没有找到相关的网络信息。"
14 |
15 | formatted_string = "根据网络搜索结果:\n\n"
16 | for i, res in enumerate(results, 1):
17 | title = res.get("title", "无标题")
18 | url = res.get("url", "#")
19 | snippet = res.get("snippet", "无摘要")
20 | provider = res.get("provider", "未知来源")
21 |
22 | formatted_string += f"{i}. **{title}** (来自: {provider})\n"
23 | formatted_string += f" - 摘要: {snippet}\n"
24 | formatted_string += f" - 来源: {url}\n\n"
25 |
26 | return formatted_string
27 |
28 |
29 | def format_url_parse_results(results: list[dict[str, Any]]) -> str:
30 | """
31 | 将成功解析的URL结果列表格式化为一段简洁的文本。
32 | """
33 | formatted_parts = []
34 | for res in results:
35 | title = res.get("title", "无标题")
36 | url = res.get("url", "#")
37 | snippet = res.get("snippet", "无摘要")
38 | source = res.get("source", "未知")
39 |
40 | formatted_string = f"**{title}**\n"
41 | formatted_string += f"**内容摘要**:\n{snippet}\n"
42 | formatted_string += f"**来源**: {url} (由 {source} 解析)\n"
43 | formatted_parts.append(formatted_string)
44 |
45 | return "\n---\n".join(formatted_parts)
46 |
47 |
48 | def deduplicate_results(results: list[dict[str, Any]]) -> list[dict[str, Any]]:
49 | """
50 | 根据URL去重搜索结果
51 | """
52 | unique_urls = set()
53 | unique_results = []
54 | for res in results:
55 | if isinstance(res, dict) and res.get("url") and res["url"] not in unique_urls:
56 | unique_urls.add(res["url"])
57 | unique_results.append(res)
58 | return unique_results
59 |
--------------------------------------------------------------------------------
/src/plugins/built_in/web_search_tool/utils/url_utils.py:
--------------------------------------------------------------------------------
1 | """
2 | URL processing utilities
3 | """
4 |
5 | import re
6 |
7 |
8 | def parse_urls_from_input(urls_input) -> list[str]:
9 | """
10 | 从输入中解析URL列表
11 | """
12 | if isinstance(urls_input, str):
13 | # 如果是字符串,尝试解析为URL列表
14 | # 提取所有HTTP/HTTPS URL
15 | url_pattern = r"https?://[^\s\],]+"
16 | urls = re.findall(url_pattern, urls_input)
17 | if not urls:
18 | # 如果没有找到标准URL,将整个字符串作为单个URL
19 | if urls_input.strip().startswith(("http://", "https://")):
20 | urls = [urls_input.strip()]
21 | else:
22 | return []
23 | elif isinstance(urls_input, list):
24 | urls = [url.strip() for url in urls_input if isinstance(url, str) and url.strip()]
25 | else:
26 | return []
27 |
28 | return urls
29 |
30 |
31 | def validate_urls(urls: list[str]) -> list[str]:
32 | """
33 | 验证URL格式,返回有效的URL列表
34 | """
35 | valid_urls = []
36 | for url in urls:
37 | if url.startswith(("http://", "https://")):
38 | valid_urls.append(url)
39 | return valid_urls
40 |
--------------------------------------------------------------------------------
/src/schedule/monthly_plan_manager.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from datetime import datetime, timedelta
3 |
4 | from src.common.logger import get_logger
5 | from src.manager.async_task_manager import AsyncTask, async_task_manager
6 |
7 | from .plan_manager import PlanManager
8 |
9 | logger = get_logger("monthly_plan_manager")
10 |
11 |
12 | class MonthlyPlanManager:
13 | def __init__(self):
14 | self.plan_manager = PlanManager()
15 | self.monthly_task_started = False
16 |
17 | async def initialize(self):
18 | logger.info("正在初始化月度计划管理器...")
19 | await self.start_monthly_plan_generation()
20 | logger.info("月度计划管理器初始化成功")
21 |
22 | async def start_monthly_plan_generation(self):
23 | if not self.monthly_task_started:
24 | logger.info(" 正在启动每月月度计划生成任务...")
25 | task = MonthlyPlanGenerationTask(self)
26 | await async_task_manager.add_task(task)
27 | self.monthly_task_started = True
28 | logger.info(" 每月月度计划生成任务已成功启动。")
29 | logger.info(" 执行启动时月度计划检查...")
30 | await self.plan_manager.ensure_and_generate_plans_if_needed()
31 | else:
32 | logger.info(" 每月月度计划生成任务已在运行中。")
33 |
34 | async def ensure_and_generate_plans_if_needed(self, target_month: str | None = None) -> bool:
35 | return await self.plan_manager.ensure_and_generate_plans_if_needed(target_month)
36 |
37 |
38 | class MonthlyPlanGenerationTask(AsyncTask):
39 | def __init__(self, monthly_plan_manager: MonthlyPlanManager):
40 | super().__init__(task_name="MonthlyPlanGenerationTask")
41 | self.monthly_plan_manager = monthly_plan_manager
42 |
43 | async def run(self):
44 | while True:
45 | try:
46 | now = datetime.now()
47 | if now.month == 12:
48 | next_month = datetime(now.year + 1, 1, 1)
49 | else:
50 | next_month = datetime(now.year, now.month + 1, 1)
51 | sleep_seconds = (next_month - now).total_seconds()
52 | logger.info(
53 | f" 下一次月度计划生成任务将在 {sleep_seconds:.2f} 秒后运行 (北京时间 {next_month.strftime('%Y-%m-%d %H:%M:%S')})"
54 | )
55 | await asyncio.sleep(sleep_seconds)
56 | last_month = (next_month - timedelta(days=1)).strftime("%Y-%m")
57 | await self.monthly_plan_manager.plan_manager.archive_current_month_plans(last_month)
58 | current_month = next_month.strftime("%Y-%m")
59 | logger.info(f" 到达月初,开始生成 {current_month} 的月度计划...")
60 | await self.monthly_plan_manager.plan_manager._generate_monthly_plans_logic(current_month)
61 | except asyncio.CancelledError:
62 | logger.info(" 每月月度计划生成任务被取消。")
63 | break
64 | except Exception as e:
65 | logger.error(f" 每月月度计划生成任务发生未知错误: {e}")
66 | await asyncio.sleep(3600)
67 |
68 |
69 | monthly_plan_manager = MonthlyPlanManager()
70 |
--------------------------------------------------------------------------------
/src/schedule/schemas.py:
--------------------------------------------------------------------------------
1 | # mmc/src/schedule/schemas.py
2 |
3 | from datetime import datetime, time
4 |
5 | from pydantic import BaseModel, validator
6 |
7 |
8 | class ScheduleItem(BaseModel):
9 | """单个日程项的Pydantic模型"""
10 |
11 | time_range: str
12 | activity: str
13 |
14 | @validator("time_range")
15 | def validate_time_range(cls, v):
16 | """验证时间范围格式"""
17 | if not v or "-" not in v:
18 | raise ValueError("时间范围必须包含'-'分隔符")
19 |
20 | try:
21 | start_str, end_str = v.split("-", 1)
22 | start_str = start_str.strip()
23 | end_str = end_str.strip()
24 |
25 | # 验证时间格式
26 | datetime.strptime(start_str, "%H:%M")
27 | datetime.strptime(end_str, "%H:%M")
28 |
29 | return v
30 | except ValueError as e:
31 | raise ValueError(f"时间格式无效,应为HH:MM-HH:MM格式: {e}") from e
32 |
33 | @validator("activity")
34 | def validate_activity(cls, v):
35 | """验证活动描述"""
36 | if not v or not v.strip():
37 | raise ValueError("活动描述不能为空")
38 | return v.strip()
39 |
40 |
41 | class ScheduleData(BaseModel):
42 | """完整日程数据的Pydantic模型"""
43 |
44 | schedule: list[ScheduleItem]
45 |
46 | @validator("schedule")
47 | def validate_schedule_completeness(cls, v):
48 | """验证日程是否覆盖24小时"""
49 | if not v:
50 | raise ValueError("日程不能为空")
51 |
52 | # 收集所有时间段
53 | time_ranges = []
54 | for item in v:
55 | try:
56 | start_str, end_str = item.time_range.split("-", 1)
57 | start_time = datetime.strptime(start_str.strip(), "%H:%M").time()
58 | end_time = datetime.strptime(end_str.strip(), "%H:%M").time()
59 | time_ranges.append((start_time, end_time))
60 | except ValueError:
61 | continue
62 |
63 | # 检查是否覆盖24小时
64 | if not cls._check_24_hour_coverage(time_ranges):
65 | raise ValueError("日程必须覆盖完整的24小时")
66 |
67 | return v
68 |
69 | @staticmethod
70 | def _check_24_hour_coverage(time_ranges: list[tuple]) -> bool:
71 | """检查时间段是否覆盖24小时"""
72 | if not time_ranges:
73 | return False
74 |
75 | # 将时间转换为分钟数进行计算
76 | def time_to_minutes(t: time) -> int:
77 | return t.hour * 60 + t.minute
78 |
79 | # 创建覆盖情况数组 (1440分钟 = 24小时)
80 | covered = [False] * 1440
81 |
82 | for start_time, end_time in time_ranges:
83 | start_min = time_to_minutes(start_time)
84 | end_min = time_to_minutes(end_time)
85 |
86 | if start_min <= end_min:
87 | # 同一天内的时间段
88 | for i in range(start_min, end_min):
89 | if i < 1440:
90 | covered[i] = True
91 | else:
92 | # 跨天的时间段
93 | for i in range(start_min, 1440):
94 | covered[i] = True
95 | for i in range(0, end_min):
96 | covered[i] = True
97 |
98 | # 检查是否所有分钟都被覆盖
99 | return all(covered)
100 |
--------------------------------------------------------------------------------
/src/utils/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 工具模块
3 | """
4 |
--------------------------------------------------------------------------------
/template/template.env:
--------------------------------------------------------------------------------
1 | HOST=127.0.0.1
2 | PORT=8000
3 | EULA_CONFIRMED=false
--------------------------------------------------------------------------------