├── .dockerignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── PLUGIN_PUBLISH.yml │ ├── bug-report.yml │ └── feature-request.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── auto_release.yml │ ├── codeql.yml │ ├── coverage_test.yml │ ├── dashboard_ci.yml │ ├── docker-image.yml │ └── stale.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .python-version ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── Dockerfile_with_node ├── LICENSE ├── README.md ├── README_en.md ├── README_ja.md ├── astrbot ├── __init__.py ├── api │ ├── __init__.py │ ├── all.py │ ├── event │ │ ├── __init__.py │ │ └── filter │ │ │ └── __init__.py │ ├── message_components.py │ ├── platform │ │ └── __init__.py │ ├── provider │ │ └── __init__.py │ ├── star │ │ └── __init__.py │ └── util │ │ └── __init__.py ├── cli │ ├── __init__.py │ ├── __main__.py │ ├── commands │ │ ├── __init__.py │ │ ├── cmd_conf.py │ │ ├── cmd_init.py │ │ ├── cmd_plug.py │ │ └── cmd_run.py │ └── utils │ │ ├── __init__.py │ │ ├── basic.py │ │ ├── plugin.py │ │ └── version_comparator.py ├── core │ ├── __init__.py │ ├── config │ │ ├── __init__.py │ │ ├── astrbot_config.py │ │ └── default.py │ ├── conversation_mgr.py │ ├── core_lifecycle.py │ ├── db │ │ ├── __init__.py │ │ ├── po.py │ │ ├── sqlite.py │ │ ├── sqlite_init.sql │ │ └── vec_db │ │ │ ├── base.py │ │ │ └── faiss_impl │ │ │ ├── __init__.py │ │ │ ├── document_storage.py │ │ │ ├── embedding_storage.py │ │ │ ├── sqlite_init.sql │ │ │ └── vec_db.py │ ├── event_bus.py │ ├── file_token_service.py │ ├── initial_loader.py │ ├── log.py │ ├── message │ │ ├── components.py │ │ └── message_event_result.py │ ├── pipeline │ │ ├── __init__.py │ │ ├── content_safety_check │ │ │ ├── stage.py │ │ │ └── strategies │ │ │ │ ├── __init__.py │ │ │ │ ├── baidu_aip.py │ │ │ │ ├── keywords.py │ │ │ │ └── strategy.py │ │ ├── context.py │ │ ├── platform_compatibility │ │ │ └── stage.py │ │ ├── preprocess_stage │ │ │ └── stage.py │ │ ├── process_stage │ │ │ ├── method │ │ │ │ ├── llm_request.py │ │ │ │ └── star_request.py │ │ │ └── stage.py │ │ ├── rate_limit_check │ │ │ └── stage.py │ │ ├── respond │ │ │ └── stage.py │ │ ├── result_decorate │ │ │ └── stage.py │ │ ├── scheduler.py │ │ ├── stage.py │ │ ├── waking_check │ │ │ └── stage.py │ │ └── whitelist_check │ │ │ └── stage.py │ ├── platform │ │ ├── __init__.py │ │ ├── astr_message_event.py │ │ ├── astrbot_message.py │ │ ├── manager.py │ │ ├── message_type.py │ │ ├── platform.py │ │ ├── platform_metadata.py │ │ ├── register.py │ │ └── sources │ │ │ ├── aiocqhttp │ │ │ ├── aiocqhttp_message_event.py │ │ │ └── aiocqhttp_platform_adapter.py │ │ │ ├── dingtalk │ │ │ ├── dingtalk_adapter.py │ │ │ └── dingtalk_event.py │ │ │ ├── gewechat │ │ │ ├── client.py │ │ │ ├── downloader.py │ │ │ ├── gewechat_event.py │ │ │ ├── gewechat_platform_adapter.py │ │ │ └── xml_data_parser.py │ │ │ ├── lark │ │ │ ├── lark_adapter.py │ │ │ └── lark_event.py │ │ │ ├── qqofficial │ │ │ ├── qqofficial_message_event.py │ │ │ └── qqofficial_platform_adapter.py │ │ │ ├── qqofficial_webhook │ │ │ ├── qo_webhook_adapter.py │ │ │ ├── qo_webhook_event.py │ │ │ └── qo_webhook_server.py │ │ │ ├── telegram │ │ │ ├── tg_adapter.py │ │ │ └── tg_event.py │ │ │ ├── webchat │ │ │ ├── webchat_adapter.py │ │ │ └── webchat_event.py │ │ │ ├── wechatpadpro │ │ │ ├── wechatpadpro_adapter.py │ │ │ └── wechatpadpro_message_event.py │ │ │ ├── wecom │ │ │ ├── wecom_adapter.py │ │ │ ├── wecom_event.py │ │ │ ├── wecom_kf.py │ │ │ └── wecom_kf_message.py │ │ │ └── weixin_official_account │ │ │ ├── weixin_offacc_adapter.py │ │ │ └── weixin_offacc_event.py │ ├── provider │ │ ├── __init__.py │ │ ├── entites.py │ │ ├── entities.py │ │ ├── func_tool_manager.py │ │ ├── manager.py │ │ ├── provider.py │ │ ├── register.py │ │ └── sources │ │ │ ├── anthropic_source.py │ │ │ ├── azure_tts_source.py │ │ │ ├── dashscope_source.py │ │ │ ├── dashscope_tts.py │ │ │ ├── dify_source.py │ │ │ ├── edge_tts_source.py │ │ │ ├── fishaudio_tts_api_source.py │ │ │ ├── gemini_embedding_source.py │ │ │ ├── gemini_source.py │ │ │ ├── gsvi_tts_source.py │ │ │ ├── llmtuner_source.py │ │ │ ├── minimax_tts_api_source.py │ │ │ ├── openai_embedding_source.py │ │ │ ├── openai_source.py │ │ │ ├── openai_tts_api_source.py │ │ │ ├── sensevoice_selfhosted_source.py │ │ │ ├── volcengine_tts.py │ │ │ ├── whisper_api_source.py │ │ │ ├── whisper_selfhosted_source.py │ │ │ └── zhipu_source.py │ ├── star │ │ ├── README.md │ │ ├── __init__.py │ │ ├── config.py │ │ ├── context.py │ │ ├── filter │ │ │ ├── __init__.py │ │ │ ├── command.py │ │ │ ├── command_group.py │ │ │ ├── custom_filter.py │ │ │ ├── event_message_type.py │ │ │ ├── permission.py │ │ │ ├── platform_adapter_type.py │ │ │ └── regex.py │ │ ├── register │ │ │ ├── __init__.py │ │ │ ├── star.py │ │ │ └── star_handler.py │ │ ├── star.py │ │ ├── star_handler.py │ │ ├── star_manager.py │ │ ├── star_tools.py │ │ └── updator.py │ ├── updator.py │ ├── utils │ │ ├── astrbot_path.py │ │ ├── command_parser.py │ │ ├── dify_api_client.py │ │ ├── io.py │ │ ├── log_pipe.py │ │ ├── metrics.py │ │ ├── path_util.py │ │ ├── pip_installer.py │ │ ├── session_waiter.py │ │ ├── shared_preferences.py │ │ ├── t2i │ │ │ ├── __init__.py │ │ │ ├── local_strategy.py │ │ │ ├── network_strategy.py │ │ │ ├── renderer.py │ │ │ └── template │ │ │ │ └── base.html │ │ ├── tencent_record_helper.py │ │ └── version_comparator.py │ └── zip_updator.py └── dashboard │ ├── routes │ ├── __init__.py │ ├── auth.py │ ├── chat.py │ ├── config.py │ ├── conversation.py │ ├── file.py │ ├── log.py │ ├── plugin.py │ ├── route.py │ ├── stat.py │ ├── static_file.py │ ├── tools.py │ └── update.py │ └── server.py ├── changelogs ├── v3.4.0.md ├── v3.4.1.md ├── v3.4.10.md ├── v3.4.11.md ├── v3.4.12.md ├── v3.4.13.md ├── v3.4.14.md ├── v3.4.15.md ├── v3.4.16.md ├── v3.4.17.md ├── v3.4.18.md ├── v3.4.19.md ├── v3.4.20.md ├── v3.4.21.md ├── v3.4.22.md ├── v3.4.23.md ├── v3.4.24.md ├── v3.4.25.md ├── v3.4.26.md ├── v3.4.27.md ├── v3.4.28.md ├── v3.4.29.md ├── v3.4.3.md ├── v3.4.30.md ├── v3.4.31.md ├── v3.4.32.md ├── v3.4.33.md ├── v3.4.35.md ├── v3.4.36.md ├── v3.4.37.md ├── v3.4.38.md ├── v3.4.39.md ├── v3.4.4.md ├── v3.4.5.md ├── v3.4.6.md ├── v3.4.7.md ├── v3.4.8.md ├── v3.4.9.md ├── v3.5.0.md ├── v3.5.1.md ├── v3.5.10.md ├── v3.5.11.md ├── v3.5.12.md ├── v3.5.13.md ├── v3.5.2.md ├── v3.5.3.1.md ├── v3.5.3.2.md ├── v3.5.3.md ├── v3.5.4.md ├── v3.5.5.md ├── v3.5.6.md ├── v3.5.7.md ├── v3.5.8.md └── v3.5.9.md ├── compose.yml ├── dashboard ├── .gitignore ├── LICENSE ├── README.md ├── env.d.ts ├── index.html ├── package.json ├── public │ ├── _redirects │ └── favicon.svg ├── src │ ├── App.vue │ ├── assets │ │ └── images │ │ │ ├── astrbot_logo_mini.webp │ │ │ ├── logo-normal.svg │ │ │ └── logo-waifu.png │ ├── components │ │ ├── ConfirmDialog.vue │ │ └── shared │ │ │ ├── AstrBotConfig.vue │ │ │ ├── ConsoleDisplayer.vue │ │ │ ├── ExtensionCard.vue │ │ │ ├── ItemCardGrid.vue │ │ │ ├── ListConfigItem.vue │ │ │ ├── Logo.vue │ │ │ ├── ReadmeDialog.vue │ │ │ ├── UiParentCard.vue │ │ │ └── WaitingForRestart.vue │ ├── config.ts │ ├── layouts │ │ ├── blank │ │ │ └── BlankLayout.vue │ │ └── full │ │ │ ├── FullLayout.vue │ │ │ ├── vertical-header │ │ │ └── VerticalHeader.vue │ │ │ └── vertical-sidebar │ │ │ ├── NavItem.vue │ │ │ ├── VerticalSidebar.vue │ │ │ └── sidebarItem.ts │ ├── main.ts │ ├── plugins │ │ ├── confirmPlugin.ts │ │ └── vuetify.ts │ ├── router │ │ ├── AuthRoutes.ts │ │ ├── MainRoutes.ts │ │ └── index.ts │ ├── scss │ │ ├── _override.scss │ │ ├── _variables.scss │ │ ├── components │ │ │ ├── _VButtons.scss │ │ │ ├── _VCard.scss │ │ │ ├── _VField.scss │ │ │ ├── _VInput.scss │ │ │ ├── _VNavigationDrawer.scss │ │ │ ├── _VShadow.scss │ │ │ ├── _VTabs.scss │ │ │ └── _VTextField.scss │ │ ├── layout │ │ │ ├── _container.scss │ │ │ └── _sidebar.scss │ │ ├── pages │ │ │ └── _dashboards.scss │ │ └── style.scss │ ├── stores │ │ ├── auth.ts │ │ ├── common.js │ │ └── customizer.ts │ ├── theme │ │ ├── DarkTheme.ts │ │ └── LightTheme.ts │ ├── types │ │ ├── themeTypes │ │ │ └── ThemeType.ts │ │ ├── vue3-print-nb.d.ts │ │ └── vue_tabler_icon.d.ts │ └── views │ │ ├── AboutPage.vue │ │ ├── AlkaidPage.vue │ │ ├── AlkaidPage_sigma.vue │ │ ├── ChatPage.vue │ │ ├── ConfigPage.vue │ │ ├── ConsolePage.vue │ │ ├── ConversationPage.vue │ │ ├── ExtensionMarketplace.vue │ │ ├── ExtensionPage.vue │ │ ├── PlatformPage.vue │ │ ├── ProviderPage.vue │ │ ├── Settings.vue │ │ ├── ToolUsePage.vue │ │ ├── alkaid │ │ ├── KnowledgeBase.vue │ │ ├── LongTermMemory.vue │ │ └── Other.vue │ │ ├── authentication │ │ ├── auth │ │ │ └── LoginPage.vue │ │ └── authForms │ │ │ └── AuthLogin.vue │ │ └── dashboards │ │ └── default │ │ ├── DefaultDashboard.vue │ │ └── components │ │ ├── MemoryUsage.vue │ │ ├── MessageStat.vue │ │ ├── OnlinePlatform.vue │ │ ├── OnlineTime.vue │ │ ├── PlatformStat.vue │ │ ├── RunningTime.vue │ │ └── TotalMessage.vue ├── tsconfig.json ├── tsconfig.vite-config.json └── vite.config.ts ├── main.py ├── packages ├── astrbot │ ├── long_term_memory.py │ └── main.py ├── python_interpreter │ ├── main.py │ ├── requirements.txt │ └── shared │ │ └── api.py ├── reminder │ └── main.py ├── session_controller │ └── main.py └── web_searcher │ ├── engines │ ├── __init__.py │ ├── bing.py │ ├── google.py │ └── sogo.py │ └── main.py ├── pyproject.toml ├── requirements.txt ├── tests ├── test_dashboard.py ├── test_main.py ├── test_pipeline.py └── test_plugin_manager.py └── uv.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 2 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 3 | # github acions 4 | .github/ 5 | .*ignore 6 | .git/ 7 | # User-specific stuff 8 | .idea/ 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | # Environments 12 | .env 13 | .venv 14 | env/ 15 | venv*/ 16 | ENV/ 17 | .conda/ 18 | README*.md 19 | dashboard/ 20 | data/ 21 | changelogs/ 22 | tests/ 23 | .ruff_cache/ 24 | .astrbot -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: astrbot 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: ['https://afdian.com/a/astrbot_team'] 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.yml: -------------------------------------------------------------------------------- 1 | name: '🥳 发布插件' 2 | title: "[Plugin] 插件名" 3 | description: 提交插件到插件市场 4 | labels: [ "plugin-publish" ] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | 欢迎发布插件到插件市场!请确保您的插件经过**完整的**测试。 10 | 11 | - type: textarea 12 | attributes: 13 | label: 插件仓库 14 | description: 插件的 GitHub 仓库链接 15 | placeholder: > 16 | 如 https://github.com/Soulter/astrbot-github-cards 17 | 18 | - type: textarea 19 | attributes: 20 | label: 描述 21 | value: | 22 | 插件名: 23 | 插件作者: 24 | 插件简介: 25 | 支持的消息平台:(必填,如 QQ、微信、飞书) 26 | 标签:(可选) 27 | 社交链接:(可选, 将会在插件市场作者名称上作为可点击的链接) 28 | description: 必填。请以列表的字段按顺序将插件名、插件作者、插件简介放在这里。如果您不知道支持哪些消息平台,请填写测试过的消息平台。 29 | 30 | - type: checkboxes 31 | attributes: 32 | label: Code of Conduct 33 | options: 34 | - label: > 35 | 我已阅读并同意遵守该项目的 [行为准则](https://docs.github.com/zh/site-policy/github-terms/github-community-code-of-conduct)。 36 | required: true 37 | 38 | - type: markdown 39 | attributes: 40 | value: "❤️" 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: '🐛 报告 Bug' 2 | title: '[Bug]' 3 | description: 提交报告帮助我们改进。 4 | labels: [ 'bug' ] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | 感谢您抽出时间报告问题!请准确解释您的问题。如果可能,请提供一个可复现的片段(这有助于更快地解决问题)。 10 | - type: textarea 11 | attributes: 12 | label: 发生了什么 13 | description: 描述你遇到的异常 14 | placeholder: > 15 | 一个清晰且具体的描述这个异常是什么。 16 | validations: 17 | required: true 18 | 19 | - type: textarea 20 | attributes: 21 | label: 如何复现? 22 | description: > 23 | 复现该问题的步骤 24 | placeholder: > 25 | 如: 1. 打开 '...' 26 | validations: 27 | required: true 28 | 29 | - type: textarea 30 | attributes: 31 | label: AstrBot 版本、部署方式(如 Windows Docker Desktop 部署)、使用的提供商、使用的消息平台适配器 32 | description: > 33 | 请提供您的 AstrBot 版本和部署方式。 34 | placeholder: > 35 | 如: 3.1.8 Docker, 3.1.7 Windows启动器 36 | validations: 37 | required: true 38 | 39 | - type: dropdown 40 | attributes: 41 | label: 操作系统 42 | description: | 43 | 你在哪个操作系统上遇到了这个问题? 44 | multiple: false 45 | options: 46 | - 'Windows' 47 | - 'macOS' 48 | - 'Linux' 49 | - 'Other' 50 | - 'Not sure' 51 | validations: 52 | required: true 53 | 54 | - type: textarea 55 | attributes: 56 | label: 报错日志 57 | description: > 58 | 如报错日志、截图等。请提供完整的 Debug 级别的日志,不要介意它很长! 59 | placeholder: > 60 | 请提供完整的报错日志或截图。 61 | validations: 62 | required: true 63 | 64 | - type: checkboxes 65 | attributes: 66 | label: 你愿意提交 PR 吗? 67 | description: > 68 | 这不是必需的,但我们很乐意在贡献过程中为您提供指导特别是如果你已经很好地理解了如何实现修复。 69 | options: 70 | - label: 是的,我愿意提交 PR! 71 | 72 | - type: checkboxes 73 | attributes: 74 | label: Code of Conduct 75 | options: 76 | - label: > 77 | 我已阅读并同意遵守该项目的 [行为准则](https://docs.github.com/zh/site-policy/github-terms/github-community-code-of-conduct)。 78 | required: true 79 | 80 | - type: markdown 81 | attributes: 82 | value: "感谢您填写我们的表单!" 83 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | 2 | name: '🎉 功能建议' 3 | title: "[Feature]" 4 | description: 提交建议帮助我们改进。 5 | labels: [ "enhancement" ] 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | 感谢您抽出时间提出新功能建议,请准确解释您的想法。 11 | 12 | - type: textarea 13 | attributes: 14 | label: 描述 15 | description: 简短描述您的功能建议。 16 | 17 | - type: textarea 18 | attributes: 19 | label: 使用场景 20 | description: 你想要发生什么? 21 | placeholder: > 22 | 一个清晰且具体的描述这个功能的使用场景。 23 | 24 | - type: checkboxes 25 | attributes: 26 | label: 你愿意提交PR吗? 27 | description: > 28 | 这不是必须的,但我们欢迎您的贡献。 29 | options: 30 | - label: 是的, 我愿意提交PR! 31 | 32 | - type: checkboxes 33 | attributes: 34 | label: Code of Conduct 35 | options: 36 | - label: > 37 | 我已阅读并同意遵守该项目的 [行为准则](https://docs.github.com/zh/site-policy/github-terms/github-community-code-of-conduct)。 38 | required: true 39 | 40 | - type: markdown 41 | attributes: 42 | value: "感谢您填写我们的表单!" -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 解决了 #XYZ 3 | 4 | ### Motivation 5 | 6 | 7 | 8 | ### Modifications 9 | 10 | 11 | 12 | ### Check 13 | 14 | 15 | 16 | - [ ] 😊 我的 Commit Message 符合良好的[规范](https://www.conventionalcommits.org/en/v1.0.0/#summary) 17 | - [ ] 👀 我的更改经过良好的测试 18 | - [ ] 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 `requirements.txt` 和 `pyproject.toml` 文件相应位置。 19 | - [ ] 😮 我的更改没有引入恶意代码 20 | -------------------------------------------------------------------------------- /.github/workflows/coverage_test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests and upload coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - 'README.md' 9 | - 'changelogs/**' 10 | - 'dashboard/**' 11 | workflow_dispatch: 12 | 13 | jobs: 14 | test: 15 | name: Run tests and collect coverage 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Set up Python 24 | uses: actions/setup-python@v4 25 | 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install -r requirements.txt 30 | pip install pytest pytest-cov pytest-asyncio 31 | 32 | - name: Run tests 33 | run: | 34 | mkdir data 35 | mkdir data/plugins 36 | mkdir data/config 37 | mkdir data/temp 38 | export TESTING=true 39 | export ZHIPU_API_KEY=${{ secrets.OPENAI_API_KEY }} 40 | PYTHONPATH=./ pytest --cov=. tests/ -v -o log_cli=true -o log_level=DEBUG 41 | 42 | - name: Upload results to Codecov 43 | uses: codecov/codecov-action@v4 44 | with: 45 | token: ${{ secrets.CODECOV_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/dashboard_ci.yml: -------------------------------------------------------------------------------- 1 | name: AstrBot Dashboard CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout repository 10 | uses: actions/checkout@v4 11 | 12 | - name: npm install, build 13 | run: | 14 | cd dashboard 15 | npm install 16 | npm run build 17 | 18 | - name: Inject Commit SHA 19 | id: get_sha 20 | run: | 21 | echo "COMMIT_SHA=$(git rev-parse HEAD)" >> $GITHUB_ENV 22 | mkdir -p dashboard/dist/assets 23 | echo $COMMIT_SHA > dashboard/dist/assets/version 24 | 25 | - name: Archive production artifacts 26 | uses: actions/upload-artifact@v4 27 | with: 28 | name: dist-without-markdown 29 | path: | 30 | dashboard/dist 31 | !dist/**/*.md 32 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI/CD 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | publish-docker: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: 拉取源码 15 | uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 1 18 | 19 | - name: 设置 QEMU 20 | uses: docker/setup-qemu-action@v3 21 | 22 | - name: 设置 Docker Buildx 23 | uses: docker/setup-buildx-action@v3 24 | 25 | - name: 登录到 DockerHub 26 | uses: docker/login-action@v3 27 | with: 28 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 29 | password: ${{ secrets.DOCKER_HUB_PASSWORD }} 30 | 31 | - name: 构建和推送 Docker hub 32 | uses: docker/build-push-action@v6 33 | with: 34 | context: . 35 | platforms: linux/amd64,linux/arm64 36 | push: true 37 | tags: | 38 | ${{ secrets.DOCKER_HUB_USERNAME }}/astrbot:latest 39 | ${{ secrets.DOCKER_HUB_USERNAME }}/astrbot:${{ github.ref_name }} 40 | 41 | - name: Post build notifications 42 | run: echo "Docker image has been built and pushed successfully" 43 | 44 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. 2 | # 3 | # You can adjust the behavior by modifying this file. 4 | # For more information, see: 5 | # https://github.com/actions/stale 6 | name: Mark stale issues and pull requests 7 | 8 | on: 9 | schedule: 10 | - cron: '21 23 * * *' 11 | 12 | jobs: 13 | stale: 14 | 15 | runs-on: ubuntu-latest 16 | permissions: 17 | issues: write 18 | pull-requests: write 19 | 20 | steps: 21 | - uses: actions/stale@v5 22 | with: 23 | repo-token: ${{ secrets.GITHUB_TOKEN }} 24 | stale-issue-message: 'Stale issue message' 25 | stale-pr-message: 'Stale pull request message' 26 | stale-issue-label: 'no-issue-activity' 27 | stale-pr-label: 'no-pr-activity' 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | botpy.log 3 | .vscode 4 | .venv* 5 | .idea 6 | data_v2.db 7 | data_v3.db 8 | configs/session 9 | configs/config.yaml 10 | **/.DS_Store 11 | temp 12 | cmd_config.json 13 | data 14 | cookies.json 15 | logs/ 16 | addons/plugins 17 | .coverage 18 | 19 | 20 | tests/astrbot_plugin_openai 21 | chroma 22 | dashboard/node_modules/ 23 | dashboard/dist/ 24 | .DS_Store 25 | package-lock.json 26 | package.json 27 | venv/* 28 | packages/python_interpreter/workplace 29 | .venv/* 30 | .conda/ 31 | .idea 32 | pytest.ini 33 | .astrbot -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_install_hook_types: [pre-commit, prepare-commit-msg] 2 | ci: 3 | autofix_commit_msg: ":balloon: auto fixes by pre-commit hooks" 4 | autofix_prs: true 5 | autoupdate_branch: master 6 | autoupdate_schedule: weekly 7 | autoupdate_commit_msg: ":balloon: pre-commit autoupdate" 8 | repos: 9 | - repo: https://github.com/astral-sh/ruff-pre-commit 10 | rev: v0.11.2 11 | hooks: 12 | - id: ruff 13 | - id: ruff-format 14 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.10 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim 2 | WORKDIR /AstrBot 3 | 4 | COPY . /AstrBot/ 5 | 6 | RUN apt-get update && apt-get install -y --no-install-recommends \ 7 | nodejs \ 8 | npm \ 9 | gcc \ 10 | build-essential \ 11 | python3-dev \ 12 | libffi-dev \ 13 | libssl-dev \ 14 | ca-certificates \ 15 | bash \ 16 | && apt-get clean \ 17 | && rm -rf /var/lib/apt/lists/* 18 | 19 | RUN python -m pip install uv 20 | RUN uv pip install -r requirements.txt --no-cache-dir --system 21 | RUN uv pip install socksio uv pyffmpeg pilk --no-cache-dir --system 22 | 23 | # 释出 ffmpeg 24 | RUN python -c "from pyffmpeg import FFmpeg; ff = FFmpeg();" 25 | 26 | # add /root/.pyffmpeg/bin/ffmpeg to PATH, inorder to use ffmpeg 27 | RUN echo 'export PATH=$PATH:/root/.pyffmpeg/bin' >> ~/.bashrc 28 | 29 | EXPOSE 6185 30 | EXPOSE 6186 31 | 32 | CMD [ "python", "main.py" ] 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Dockerfile_with_node: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim 2 | 3 | WORKDIR /AstrBot 4 | 5 | COPY . /AstrBot/ 6 | 7 | RUN apt-get update && apt-get install -y --no-install-recommends \ 8 | gcc \ 9 | build-essential \ 10 | python3-dev \ 11 | libffi-dev \ 12 | libssl-dev \ 13 | curl \ 14 | unzip \ 15 | ca-certificates \ 16 | bash \ 17 | && apt-get clean \ 18 | && rm -rf /var/lib/apt/lists/* 19 | 20 | # Installation of Node.js 21 | ENV NVM_DIR="/root/.nvm" 22 | RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash && \ 23 | . "$NVM_DIR/nvm.sh" && \ 24 | nvm install 22 && \ 25 | nvm use 22 26 | RUN /bin/bash -c ". \"$NVM_DIR/nvm.sh\" && node -v && npm -v" 27 | 28 | RUN python -m pip install uv 29 | RUN uv pip install -r requirements.txt --no-cache-dir --system 30 | RUN uv pip install socksio uv pyffmpeg --no-cache-dir --system 31 | 32 | EXPOSE 6185 33 | EXPOSE 6186 34 | 35 | CMD ["python", "main.py"] 36 | -------------------------------------------------------------------------------- /astrbot/__init__.py: -------------------------------------------------------------------------------- 1 | from .core.log import LogManager 2 | 3 | logger = LogManager.GetLogger(log_name="astrbot") 4 | -------------------------------------------------------------------------------- /astrbot/api/__init__.py: -------------------------------------------------------------------------------- 1 | from astrbot.core.config.astrbot_config import AstrBotConfig 2 | from astrbot import logger 3 | from astrbot.core import html_renderer 4 | from astrbot.core import sp 5 | from astrbot.core.star.register import register_llm_tool as llm_tool 6 | 7 | __all__ = ["AstrBotConfig", "logger", "html_renderer", "llm_tool", "sp"] 8 | -------------------------------------------------------------------------------- /astrbot/api/all.py: -------------------------------------------------------------------------------- 1 | from astrbot.core.config.astrbot_config import AstrBotConfig 2 | from astrbot import logger 3 | from astrbot.core import html_renderer 4 | from astrbot.core.star.register import register_llm_tool as llm_tool 5 | 6 | # event 7 | from astrbot.core.message.message_event_result import ( 8 | MessageEventResult, 9 | MessageChain, 10 | CommandResult, 11 | EventResultType, 12 | ) 13 | from astrbot.core.platform import AstrMessageEvent 14 | 15 | # star register 16 | from astrbot.core.star.register import ( 17 | register_command as command, 18 | register_command_group as command_group, 19 | register_event_message_type as event_message_type, 20 | register_regex as regex, 21 | register_platform_adapter_type as platform_adapter_type, 22 | ) 23 | from astrbot.core.star.filter.event_message_type import ( 24 | EventMessageTypeFilter, 25 | EventMessageType, 26 | ) 27 | from astrbot.core.star.filter.platform_adapter_type import ( 28 | PlatformAdapterTypeFilter, 29 | PlatformAdapterType, 30 | ) 31 | from astrbot.core.star.register import ( 32 | register_star as register, # 注册插件(Star) 33 | ) 34 | from astrbot.core.star import Context, Star 35 | from astrbot.core.star.config import * 36 | 37 | 38 | # provider 39 | from astrbot.core.provider import Provider, Personality, ProviderMetaData 40 | 41 | # platform 42 | from astrbot.core.platform import ( 43 | AstrMessageEvent, 44 | Platform, 45 | AstrBotMessage, 46 | MessageMember, 47 | MessageType, 48 | PlatformMetadata, 49 | ) 50 | 51 | from astrbot.core.platform.register import register_platform_adapter 52 | 53 | from .message_components import * -------------------------------------------------------------------------------- /astrbot/api/event/__init__.py: -------------------------------------------------------------------------------- 1 | from astrbot.core.message.message_event_result import ( 2 | MessageEventResult, 3 | MessageChain, 4 | CommandResult, 5 | EventResultType, 6 | ResultContentType, 7 | ) 8 | 9 | from astrbot.core.platform import AstrMessageEvent 10 | 11 | __all__ = [ 12 | "MessageEventResult", 13 | "MessageChain", 14 | "CommandResult", 15 | "EventResultType", 16 | "AstrMessageEvent", 17 | "ResultContentType", 18 | ] 19 | -------------------------------------------------------------------------------- /astrbot/api/event/filter/__init__.py: -------------------------------------------------------------------------------- 1 | from astrbot.core.star.register import ( 2 | register_command as command, 3 | register_command_group as command_group, 4 | register_event_message_type as event_message_type, 5 | register_regex as regex, 6 | register_platform_adapter_type as platform_adapter_type, 7 | register_permission_type as permission_type, 8 | register_custom_filter as custom_filter, 9 | register_on_astrbot_loaded as on_astrbot_loaded, 10 | register_on_llm_request as on_llm_request, 11 | register_on_llm_response as on_llm_response, 12 | register_llm_tool as llm_tool, 13 | register_on_decorating_result as on_decorating_result, 14 | register_after_message_sent as after_message_sent, 15 | ) 16 | 17 | from astrbot.core.star.filter.event_message_type import ( 18 | EventMessageTypeFilter, 19 | EventMessageType, 20 | ) 21 | from astrbot.core.star.filter.platform_adapter_type import ( 22 | PlatformAdapterTypeFilter, 23 | PlatformAdapterType, 24 | ) 25 | from astrbot.core.star.filter.permission import PermissionTypeFilter, PermissionType 26 | from astrbot.core.star.filter.custom_filter import CustomFilter 27 | 28 | __all__ = [ 29 | "command", 30 | "command_group", 31 | "event_message_type", 32 | "regex", 33 | "platform_adapter_type", 34 | "permission_type", 35 | "EventMessageTypeFilter", 36 | "EventMessageType", 37 | "PlatformAdapterTypeFilter", 38 | "PlatformAdapterType", 39 | "PermissionTypeFilter", 40 | "CustomFilter", 41 | "custom_filter", 42 | "PermissionType", 43 | "on_astrbot_loaded", 44 | "on_llm_request", 45 | "llm_tool", 46 | "on_decorating_result", 47 | "after_message_sent", 48 | "on_llm_response", 49 | ] 50 | -------------------------------------------------------------------------------- /astrbot/api/message_components.py: -------------------------------------------------------------------------------- 1 | from astrbot.core.message.components import * 2 | -------------------------------------------------------------------------------- /astrbot/api/platform/__init__.py: -------------------------------------------------------------------------------- 1 | from astrbot.core.platform import ( 2 | AstrMessageEvent, 3 | Platform, 4 | AstrBotMessage, 5 | MessageMember, 6 | MessageType, 7 | PlatformMetadata, 8 | Group, 9 | ) 10 | 11 | from astrbot.core.platform.register import register_platform_adapter 12 | from astrbot.core.message.components import * 13 | 14 | __all__ = [ 15 | "AstrMessageEvent", 16 | "Platform", 17 | "AstrBotMessage", 18 | "MessageMember", 19 | "MessageType", 20 | "PlatformMetadata", 21 | "register_platform_adapter", 22 | "Group", 23 | ] 24 | -------------------------------------------------------------------------------- /astrbot/api/provider/__init__.py: -------------------------------------------------------------------------------- 1 | from astrbot.core.provider import Provider, STTProvider, Personality 2 | from astrbot.core.provider.entities import ( 3 | ProviderRequest, 4 | ProviderType, 5 | ProviderMetaData, 6 | LLMResponse, 7 | ) 8 | 9 | __all__ = [ 10 | "Provider", 11 | "STTProvider", 12 | "Personality", 13 | "ProviderRequest", 14 | "ProviderType", 15 | "ProviderMetaData", 16 | "LLMResponse", 17 | ] 18 | -------------------------------------------------------------------------------- /astrbot/api/star/__init__.py: -------------------------------------------------------------------------------- 1 | from astrbot.core.star.register import ( 2 | register_star as register, # 注册插件(Star) 3 | ) 4 | 5 | from astrbot.core.star import Context, Star, StarTools 6 | from astrbot.core.star.config import * 7 | 8 | __all__ = ["register", "Context", "Star", "StarTools"] 9 | -------------------------------------------------------------------------------- /astrbot/api/util/__init__.py: -------------------------------------------------------------------------------- 1 | from astrbot.core.utils.session_waiter import ( 2 | SessionWaiter, 3 | SessionController, 4 | session_waiter, 5 | ) 6 | 7 | __all__ = ["SessionWaiter", "SessionController", "session_waiter"] 8 | -------------------------------------------------------------------------------- /astrbot/cli/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "3.5.8" 2 | -------------------------------------------------------------------------------- /astrbot/cli/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | AstrBot CLI入口 3 | """ 4 | 5 | import click 6 | import sys 7 | from . import __version__ 8 | from .commands import init, run, plug, conf 9 | 10 | logo_tmpl = r""" 11 | ___ _______.___________..______ .______ ______ .___________. 12 | / \ / | || _ \ | _ \ / __ \ | | 13 | / ^ \ | (----`---| |----`| |_) | | |_) | | | | | `---| |----` 14 | / /_\ \ \ \ | | | / | _ < | | | | | | 15 | / _____ \ .----) | | | | |\ \----.| |_) | | `--' | | | 16 | /__/ \__\ |_______/ |__| | _| `._____||______/ \______/ |__| 17 | """ 18 | 19 | 20 | @click.group() 21 | @click.version_option(__version__, prog_name="AstrBot") 22 | def cli() -> None: 23 | """The AstrBot CLI""" 24 | click.echo(logo_tmpl) 25 | click.echo("Welcome to AstrBot CLI!") 26 | click.echo(f"AstrBot CLI version: {__version__}") 27 | 28 | 29 | @click.command() 30 | @click.argument("command_name", required=False, type=str) 31 | def help(command_name: str | None) -> None: 32 | """显示命令的帮助信息 33 | 34 | 如果提供了 COMMAND_NAME,则显示该命令的详细帮助信息。 35 | 否则,显示通用帮助信息。 36 | """ 37 | ctx = click.get_current_context() 38 | if command_name: 39 | # 查找指定命令 40 | command = cli.get_command(ctx, command_name) 41 | if command: 42 | # 显示特定命令的帮助信息 43 | click.echo(command.get_help(ctx)) 44 | else: 45 | click.echo(f"Unknown command: {command_name}") 46 | sys.exit(1) 47 | else: 48 | # 显示通用帮助信息 49 | click.echo(cli.get_help(ctx)) 50 | 51 | 52 | cli.add_command(init) 53 | cli.add_command(run) 54 | cli.add_command(help) 55 | cli.add_command(plug) 56 | cli.add_command(conf) 57 | 58 | if __name__ == "__main__": 59 | cli() 60 | -------------------------------------------------------------------------------- /astrbot/cli/commands/__init__.py: -------------------------------------------------------------------------------- 1 | from .cmd_init import init 2 | from .cmd_run import run 3 | from .cmd_plug import plug 4 | from .cmd_conf import conf 5 | 6 | __all__ = ["init", "run", "plug", "conf"] 7 | -------------------------------------------------------------------------------- /astrbot/cli/commands/cmd_init.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import click 4 | from filelock import FileLock, Timeout 5 | 6 | from ..utils import check_dashboard, get_astrbot_root 7 | 8 | 9 | async def initialize_astrbot(astrbot_root) -> None: 10 | """执行 AstrBot 初始化逻辑""" 11 | dot_astrbot = astrbot_root / ".astrbot" 12 | 13 | if not dot_astrbot.exists(): 14 | click.echo(f"Current Directory: {astrbot_root}") 15 | click.echo( 16 | "如果你确认这是 Astrbot root directory, 你需要在当前目录下创建一个 .astrbot 文件标记该目录为 AstrBot 的数据目录。" 17 | ) 18 | if click.confirm( 19 | f"请检查当前目录是否正确,确认正确请回车: {astrbot_root}", 20 | default=True, 21 | abort=True, 22 | ): 23 | dot_astrbot.touch() 24 | click.echo(f"Created {dot_astrbot}") 25 | 26 | paths = { 27 | "data": astrbot_root / "data", 28 | "config": astrbot_root / "data" / "config", 29 | "plugins": astrbot_root / "data" / "plugins", 30 | "temp": astrbot_root / "data" / "temp", 31 | } 32 | 33 | for name, path in paths.items(): 34 | path.mkdir(parents=True, exist_ok=True) 35 | click.echo(f"{'Created' if not path.exists() else 'Directory exists'}: {path}") 36 | 37 | await check_dashboard(astrbot_root / "data") 38 | 39 | 40 | @click.command() 41 | def init() -> None: 42 | """初始化 AstrBot""" 43 | click.echo("Initializing AstrBot...") 44 | astrbot_root = get_astrbot_root() 45 | lock_file = astrbot_root / "astrbot.lock" 46 | lock = FileLock(lock_file, timeout=5) 47 | 48 | try: 49 | with lock.acquire(): 50 | asyncio.run(initialize_astrbot(astrbot_root)) 51 | except Timeout: 52 | raise click.ClickException("无法获取锁文件,请检查是否有其他实例正在运行") 53 | 54 | except Exception as e: 55 | raise click.ClickException(f"初始化失败: {e!s}") 56 | -------------------------------------------------------------------------------- /astrbot/cli/commands/cmd_run.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from pathlib import Path 4 | 5 | import click 6 | import asyncio 7 | import traceback 8 | 9 | from filelock import FileLock, Timeout 10 | 11 | from ..utils import check_dashboard, check_astrbot_root, get_astrbot_root 12 | 13 | 14 | async def run_astrbot(astrbot_root: Path): 15 | """运行 AstrBot""" 16 | from astrbot.core import logger, LogManager, LogBroker, db_helper 17 | from astrbot.core.initial_loader import InitialLoader 18 | 19 | await check_dashboard(astrbot_root / "data") 20 | 21 | log_broker = LogBroker() 22 | LogManager.set_queue_handler(logger, log_broker) 23 | db = db_helper 24 | 25 | core_lifecycle = InitialLoader(db, log_broker) 26 | 27 | await core_lifecycle.start() 28 | 29 | 30 | @click.option("--reload", "-r", is_flag=True, help="插件自动重载") 31 | @click.option("--port", "-p", help="Astrbot Dashboard端口", required=False, type=str) 32 | @click.command() 33 | def run(reload: bool, port: str) -> None: 34 | """运行 AstrBot""" 35 | try: 36 | os.environ["ASTRBOT_CLI"] = "1" 37 | astrbot_root = get_astrbot_root() 38 | 39 | if not check_astrbot_root(astrbot_root): 40 | raise click.ClickException( 41 | f"{astrbot_root}不是有效的 AstrBot 根目录,如需初始化请使用 astrbot init" 42 | ) 43 | 44 | os.environ["ASTRBOT_ROOT"] = str(astrbot_root) 45 | sys.path.insert(0, str(astrbot_root)) 46 | 47 | if port: 48 | os.environ["DASHBOARD_PORT"] = port 49 | 50 | if reload: 51 | click.echo("启用插件自动重载") 52 | os.environ["ASTRBOT_RELOAD"] = "1" 53 | 54 | lock_file = astrbot_root / "astrbot.lock" 55 | lock = FileLock(lock_file, timeout=5) 56 | with lock.acquire(): 57 | asyncio.run(run_astrbot(astrbot_root)) 58 | except KeyboardInterrupt: 59 | click.echo("AstrBot 已关闭...") 60 | except Timeout: 61 | raise click.ClickException("无法获取锁文件,请检查是否有其他实例正在运行") 62 | except Exception as e: 63 | raise click.ClickException(f"运行时出现错误: {e}\n{traceback.format_exc()}") 64 | -------------------------------------------------------------------------------- /astrbot/cli/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .basic import ( 2 | get_astrbot_root, 3 | check_astrbot_root, 4 | check_dashboard, 5 | ) 6 | from .plugin import get_git_repo, manage_plugin, build_plug_list, PluginStatus 7 | from .version_comparator import VersionComparator 8 | 9 | __all__ = [ 10 | "get_astrbot_root", 11 | "check_astrbot_root", 12 | "check_dashboard", 13 | "get_git_repo", 14 | "manage_plugin", 15 | "build_plug_list", 16 | "VersionComparator", 17 | "PluginStatus", 18 | ] 19 | -------------------------------------------------------------------------------- /astrbot/cli/utils/basic.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import click 4 | 5 | 6 | def check_astrbot_root(path: str | Path) -> bool: 7 | """检查路径是否为 AstrBot 根目录""" 8 | if not isinstance(path, Path): 9 | path = Path(path) 10 | if not path.exists() or not path.is_dir(): 11 | return False 12 | if not (path / ".astrbot").exists(): 13 | return False 14 | return True 15 | 16 | 17 | def get_astrbot_root() -> Path: 18 | """获取Astrbot根目录路径""" 19 | return Path.cwd() 20 | 21 | 22 | async def check_dashboard(astrbot_root: Path) -> None: 23 | """检查是否安装了dashboard""" 24 | from astrbot.core.utils.io import get_dashboard_version, download_dashboard 25 | from astrbot.core.config.default import VERSION 26 | from .version_comparator import VersionComparator 27 | 28 | try: 29 | dashboard_version = await get_dashboard_version() 30 | match dashboard_version: 31 | case None: 32 | click.echo("未安装管理面板") 33 | if click.confirm( 34 | "是否安装管理面板?", 35 | default=True, 36 | abort=True, 37 | ): 38 | click.echo("正在安装管理面板...") 39 | await download_dashboard( 40 | path="data/dashboard.zip", extract_path=str(astrbot_root) 41 | ) 42 | click.echo("管理面板安装完成") 43 | 44 | case str(): 45 | if VersionComparator.compare_version(VERSION, dashboard_version) <= 0: 46 | click.echo("管理面板已是最新版本") 47 | return 48 | else: 49 | try: 50 | version = dashboard_version.split("v")[1] 51 | click.echo(f"管理面板版本: {version}") 52 | await download_dashboard( 53 | path="data/dashboard.zip", extract_path=str(astrbot_root) 54 | ) 55 | except Exception as e: 56 | click.echo(f"下载管理面板失败: {e}") 57 | return 58 | except FileNotFoundError: 59 | click.echo("初始化管理面板目录...") 60 | try: 61 | await download_dashboard( 62 | path=str(astrbot_root / "dashboard.zip"), extract_path=str(astrbot_root) 63 | ) 64 | click.echo("管理面板初始化完成") 65 | except Exception as e: 66 | click.echo(f"下载管理面板失败: {e}") 67 | return 68 | -------------------------------------------------------------------------------- /astrbot/core/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import asyncio 3 | from .log import LogManager, LogBroker # noqa 4 | from astrbot.core.utils.t2i.renderer import HtmlRenderer 5 | from astrbot.core.utils.shared_preferences import SharedPreferences 6 | from astrbot.core.utils.pip_installer import PipInstaller 7 | from astrbot.core.db.sqlite import SQLiteDatabase 8 | from astrbot.core.config.default import DB_PATH 9 | from astrbot.core.config import AstrBotConfig 10 | from astrbot.core.file_token_service import FileTokenService 11 | from .utils.astrbot_path import get_astrbot_data_path 12 | 13 | # 初始化数据存储文件夹 14 | os.makedirs(get_astrbot_data_path(), exist_ok=True) 15 | 16 | WEBUI_SK = "Advanced_System_for_Text_Response_and_Bot_Operations_Tool" 17 | DEMO_MODE = os.getenv("DEMO_MODE", False) 18 | 19 | astrbot_config = AstrBotConfig() 20 | t2i_base_url = astrbot_config.get("t2i_endpoint", "https://t2i.soulter.top/text2img") 21 | html_renderer = HtmlRenderer(t2i_base_url) 22 | logger = LogManager.GetLogger(log_name="astrbot") 23 | db_helper = SQLiteDatabase(DB_PATH) 24 | # 简单的偏好设置存储, 这里后续应该存储到数据库中, 一些部分可以存储到配置中 25 | sp = SharedPreferences() 26 | # 文件令牌服务 27 | file_token_service = FileTokenService() 28 | pip_installer = PipInstaller( 29 | astrbot_config.get("pip_install_arg", ""), 30 | astrbot_config.get("pypi_index_url", None), 31 | ) 32 | web_chat_queue = asyncio.Queue(maxsize=32) 33 | web_chat_back_queue = asyncio.Queue(maxsize=32) 34 | 35 | -------------------------------------------------------------------------------- /astrbot/core/config/__init__.py: -------------------------------------------------------------------------------- 1 | from .default import DEFAULT_CONFIG, VERSION, DB_PATH 2 | from .astrbot_config import * 3 | 4 | __all__ = [ 5 | "DEFAULT_CONFIG", 6 | "VERSION", 7 | "DB_PATH", 8 | "AstrBotConfig", 9 | ] 10 | -------------------------------------------------------------------------------- /astrbot/core/db/po.py: -------------------------------------------------------------------------------- 1 | """指标数据""" 2 | 3 | from dataclasses import dataclass, field 4 | from typing import List 5 | 6 | 7 | @dataclass 8 | class Platform: 9 | """平台使用统计数据""" 10 | 11 | name: str 12 | count: int 13 | timestamp: int 14 | 15 | 16 | @dataclass 17 | class Provider: 18 | """供应商使用统计数据""" 19 | 20 | name: str 21 | count: int 22 | timestamp: int 23 | 24 | 25 | @dataclass 26 | class Plugin: 27 | """插件使用统计数据""" 28 | 29 | name: str 30 | count: int 31 | timestamp: int 32 | 33 | 34 | @dataclass 35 | class Command: 36 | """命令使用统计数据""" 37 | 38 | name: str 39 | count: int 40 | timestamp: int 41 | 42 | 43 | @dataclass 44 | class Stats: 45 | platform: List[Platform] = field(default_factory=list) 46 | command: List[Command] = field(default_factory=list) 47 | llm: List[Provider] = field(default_factory=list) 48 | 49 | 50 | @dataclass 51 | class LLMHistory: 52 | """LLM 聊天时持久化的信息""" 53 | 54 | provider_type: str 55 | session_id: str 56 | content: str 57 | 58 | 59 | @dataclass 60 | class ATRIVision: 61 | """Deprecated""" 62 | 63 | id: str 64 | url_or_path: str 65 | caption: str 66 | is_meme: bool 67 | keywords: List[str] 68 | platform_name: str 69 | session_id: str 70 | sender_nickname: str 71 | timestamp: int = -1 72 | 73 | 74 | @dataclass 75 | class Conversation: 76 | """LLM 对话存储 77 | 78 | 对于网页聊天,history 存储了包括指令、回复、图片等在内的所有消息。 79 | 对于其他平台的聊天,不存储非 LLM 的回复(因为考虑到已经存储在各自的平台上)。 80 | """ 81 | 82 | user_id: str 83 | cid: str 84 | history: str = "" 85 | """字符串格式的列表。""" 86 | created_at: int = 0 87 | updated_at: int = 0 88 | title: str = "" 89 | persona_id: str = "" 90 | -------------------------------------------------------------------------------- /astrbot/core/db/sqlite_init.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS platform( 2 | name VARCHAR(32), 3 | count INTEGER, 4 | timestamp INTEGER 5 | ); 6 | CREATE TABLE IF NOT EXISTS llm( 7 | name VARCHAR(32), 8 | count INTEGER, 9 | timestamp INTEGER 10 | ); 11 | CREATE TABLE IF NOT EXISTS plugin( 12 | name VARCHAR(32), 13 | count INTEGER, 14 | timestamp INTEGER 15 | ); 16 | CREATE TABLE IF NOT EXISTS command( 17 | name VARCHAR(32), 18 | count INTEGER, 19 | timestamp INTEGER 20 | ); 21 | CREATE TABLE IF NOT EXISTS llm_history( 22 | provider_type VARCHAR(32), 23 | session_id VARCHAR(32), 24 | content TEXT 25 | ); 26 | 27 | -- ATRI 28 | CREATE TABLE IF NOT EXISTS atri_vision( 29 | id TEXT, 30 | url_or_path TEXT, 31 | caption TEXT, 32 | is_meme BOOLEAN, 33 | keywords TEXT, 34 | platform_name VARCHAR(32), 35 | session_id VARCHAR(32), 36 | sender_nickname VARCHAR(32), 37 | timestamp INTEGER 38 | ); 39 | 40 | CREATE TABLE IF NOT EXISTS webchat_conversation( 41 | user_id TEXT, -- 会话 id 42 | cid TEXT, -- 对话 id 43 | history TEXT, 44 | created_at INTEGER, 45 | updated_at INTEGER, 46 | title TEXT, 47 | persona_id TEXT 48 | ); 49 | 50 | PRAGMA encoding = 'UTF-8'; -------------------------------------------------------------------------------- /astrbot/core/db/vec_db/base.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from dataclasses import dataclass 3 | 4 | 5 | @dataclass 6 | class Result: 7 | similarity: float 8 | data: dict 9 | 10 | 11 | class BaseVecDB: 12 | async def initialize(self): 13 | """ 14 | 初始化向量数据库 15 | """ 16 | pass 17 | 18 | @abc.abstractmethod 19 | async def insert(self, content: str, metadata: dict = None, id: str = None) -> int: 20 | """ 21 | 插入一条文本和其对应向量,自动生成 ID 并保持一致性。 22 | """ 23 | ... 24 | 25 | @abc.abstractmethod 26 | async def retrieve(self, query: str, top_k: int = 5) -> list[Result]: 27 | """ 28 | 搜索最相似的文档。 29 | Args: 30 | query (str): 查询文本 31 | top_k (int): 返回的最相似文档的数量 32 | Returns: 33 | List[Result]: 查询结果 34 | """ 35 | ... 36 | 37 | @abc.abstractmethod 38 | async def delete(self, doc_id: str) -> bool: 39 | """ 40 | 删除指定文档。 41 | Args: 42 | doc_id (str): 要删除的文档 ID 43 | Returns: 44 | bool: 删除是否成功 45 | """ 46 | ... 47 | -------------------------------------------------------------------------------- /astrbot/core/db/vec_db/faiss_impl/__init__.py: -------------------------------------------------------------------------------- 1 | from .vec_db import FaissVecDB 2 | 3 | __all__ = ["FaissVecDB"] -------------------------------------------------------------------------------- /astrbot/core/db/vec_db/faiss_impl/embedding_storage.py: -------------------------------------------------------------------------------- 1 | try: 2 | import faiss 3 | except ModuleNotFoundError: 4 | raise ImportError( 5 | "faiss 未安装。请使用 'pip install faiss-cpu' 或 'pip install faiss-gpu' 安装。" 6 | ) 7 | import os 8 | import numpy as np 9 | 10 | 11 | class EmbeddingStorage: 12 | def __init__(self, dimension: int, path: str = None): 13 | self.dimension = dimension 14 | self.path = path 15 | self.index = None 16 | if path and os.path.exists(path): 17 | self.index = faiss.read_index(path) 18 | else: 19 | base_index = faiss.IndexFlatL2(dimension) 20 | self.index = faiss.IndexIDMap(base_index) 21 | self.storage = {} 22 | 23 | async def insert(self, vector: np.ndarray, id: int): 24 | """插入向量 25 | 26 | Args: 27 | vector (np.ndarray): 要插入的向量 28 | id (int): 向量的ID 29 | Raises: 30 | ValueError: 如果向量的维度与存储的维度不匹配 31 | """ 32 | if vector.shape[0] != self.dimension: 33 | raise ValueError( 34 | f"向量维度不匹配, 期望: {self.dimension}, 实际: {vector.shape[0]}" 35 | ) 36 | self.index.add_with_ids(vector.reshape(1, -1), np.array([id])) 37 | self.storage[id] = vector 38 | await self.save_index() 39 | 40 | async def search(self, vector: np.ndarray, k: int) -> tuple: 41 | """搜索最相似的向量 42 | 43 | Args: 44 | vector (np.ndarray): 查询向量 45 | k (int): 返回的最相似向量的数量 46 | Returns: 47 | tuple: (距离, 索引) 48 | """ 49 | faiss.normalize_L2(vector) 50 | distances, indices = self.index.search(vector, k) 51 | return distances, indices 52 | 53 | async def save_index(self): 54 | """保存索引 55 | 56 | Args: 57 | path (str): 保存索引的路径 58 | """ 59 | faiss.write_index(self.index, self.path) 60 | -------------------------------------------------------------------------------- /astrbot/core/db/vec_db/faiss_impl/sqlite_init.sql: -------------------------------------------------------------------------------- 1 | -- 创建文档存储表,包含 faiss 中文档的 id,文档文本,create_at,updated_at 2 | CREATE TABLE documents ( 3 | id INTEGER PRIMARY KEY AUTOINCREMENT, 4 | doc_id TEXT NOT NULL, 5 | text TEXT NOT NULL, 6 | metadata TEXT, 7 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 8 | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP 9 | ); 10 | 11 | ALTER TABLE documents 12 | ADD COLUMN group_id TEXT GENERATED ALWAYS AS (json_extract(metadata, '$.group_id')) STORED; 13 | ALTER TABLE documents 14 | ADD COLUMN user_id TEXT GENERATED ALWAYS AS (json_extract(metadata, '$.user_id')) STORED; 15 | 16 | CREATE INDEX idx_documents_user_id ON documents(user_id); 17 | CREATE INDEX idx_documents_group_id ON documents(group_id); -------------------------------------------------------------------------------- /astrbot/core/event_bus.py: -------------------------------------------------------------------------------- 1 | """ 2 | 事件总线, 用于处理事件的分发和处理 3 | 事件总线是一个异步队列, 用于接收各种消息事件, 并将其发送到Scheduler调度器进行处理 4 | 其中包含了一个无限循环的调度函数, 用于从事件队列中获取新的事件, 并创建一个新的异步任务来执行管道调度器的处理逻辑 5 | 6 | class: 7 | EventBus: 事件总线, 用于处理事件的分发和处理 8 | 9 | 工作流程: 10 | 1. 维护一个异步队列, 来接受各种消息事件 11 | 2. 无限循环的调度函数, 从事件队列中获取新的事件, 打印日志并创建一个新的异步任务来执行管道调度器的处理逻辑 12 | """ 13 | 14 | import asyncio 15 | from asyncio import Queue 16 | from astrbot.core.pipeline.scheduler import PipelineScheduler 17 | from astrbot.core import logger 18 | from .platform import AstrMessageEvent 19 | 20 | 21 | class EventBus: 22 | """事件总线: 用于处理事件的分发和处理 23 | 24 | 维护一个异步队列, 来接受各种消息事件 25 | """ 26 | 27 | def __init__(self, event_queue: Queue, pipeline_scheduler: PipelineScheduler): 28 | self.event_queue = event_queue # 事件队列 29 | self.pipeline_scheduler = pipeline_scheduler # 管道调度器 30 | 31 | async def dispatch(self): 32 | """无限循环的调度函数, 从事件队列中获取新的事件, 打印日志并创建一个新的异步任务来执行管道调度器的处理逻辑""" 33 | while True: 34 | event: AstrMessageEvent = ( 35 | await self.event_queue.get() 36 | ) # 从事件队列中获取新的事件 37 | self._print_event(event) # 打印日志 38 | asyncio.create_task( 39 | self.pipeline_scheduler.execute(event) 40 | ) # 创建新的异步任务来执行管道调度器的处理逻辑 41 | 42 | def _print_event(self, event: AstrMessageEvent): 43 | """用于记录事件信息 44 | 45 | Args: 46 | event (AstrMessageEvent): 事件对象 47 | """ 48 | # 如果有发送者名称: [平台名] 发送者名称/发送者ID: 消息概要 49 | if event.get_sender_name(): 50 | logger.info( 51 | f"[{event.get_platform_name()}] {event.get_sender_name()}/{event.get_sender_id()}: {event.get_message_outline()}" 52 | ) 53 | # 没有发送者名称: [平台名] 发送者ID: 消息概要 54 | else: 55 | logger.info( 56 | f"[{event.get_platform_name()}] {event.get_sender_id()}: {event.get_message_outline()}" 57 | ) 58 | -------------------------------------------------------------------------------- /astrbot/core/file_token_service.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import uuid 4 | import time 5 | 6 | 7 | class FileTokenService: 8 | """维护一个简单的基于令牌的文件下载服务,支持超时和懒清除。""" 9 | 10 | def __init__(self, default_timeout: float = 300): 11 | self.lock = asyncio.Lock() 12 | self.staged_files = {} # token: (file_path, expire_time) 13 | self.default_timeout = default_timeout 14 | 15 | async def _cleanup_expired_tokens(self): 16 | """清理过期的令牌""" 17 | now = time.time() 18 | expired_tokens = [token for token, (_, expire) in self.staged_files.items() if expire < now] 19 | for token in expired_tokens: 20 | self.staged_files.pop(token, None) 21 | 22 | async def register_file(self, file_path: str, timeout: float = None) -> str: 23 | """向令牌服务注册一个文件。 24 | 25 | Args: 26 | file_path(str): 文件路径 27 | timeout(float): 超时时间,单位秒(可选) 28 | 29 | Returns: 30 | str: 一个单次令牌 31 | 32 | Raises: 33 | FileNotFoundError: 当路径不存在时抛出 34 | """ 35 | async with self.lock: 36 | await self._cleanup_expired_tokens() 37 | 38 | if not os.path.exists(file_path): 39 | raise FileNotFoundError(f"文件不存在: {file_path}") 40 | 41 | file_token = str(uuid.uuid4()) 42 | expire_time = time.time() + (timeout if timeout is not None else self.default_timeout) 43 | self.staged_files[file_token] = (file_path, expire_time) 44 | return file_token 45 | 46 | async def handle_file(self, file_token: str) -> str: 47 | """根据令牌获取文件路径,使用后令牌失效。 48 | 49 | Args: 50 | file_token(str): 注册时返回的令牌 51 | 52 | Returns: 53 | str: 文件路径 54 | 55 | Raises: 56 | KeyError: 当令牌不存在或已过期时抛出 57 | FileNotFoundError: 当文件本身已被删除时抛出 58 | """ 59 | async with self.lock: 60 | await self._cleanup_expired_tokens() 61 | 62 | if file_token not in self.staged_files: 63 | raise KeyError(f"无效或过期的文件 token: {file_token}") 64 | 65 | file_path, _ = self.staged_files.pop(file_token) 66 | if not os.path.exists(file_path): 67 | raise FileNotFoundError(f"文件不存在: {file_path}") 68 | return file_path 69 | -------------------------------------------------------------------------------- /astrbot/core/initial_loader.py: -------------------------------------------------------------------------------- 1 | """ 2 | AstrBot 启动器,负责初始化和启动核心组件和仪表板服务器。 3 | 4 | 工作流程: 5 | 1. 初始化核心生命周期, 传递数据库和日志代理实例到核心生命周期 6 | 2. 运行核心生命周期任务和仪表板服务器 7 | """ 8 | 9 | import asyncio 10 | import traceback 11 | from astrbot.core import logger 12 | from astrbot.core.core_lifecycle import AstrBotCoreLifecycle 13 | from astrbot.core.db import BaseDatabase 14 | from astrbot.core import LogBroker 15 | from astrbot.dashboard.server import AstrBotDashboard 16 | 17 | 18 | class InitialLoader: 19 | """AstrBot 启动器,负责初始化和启动核心组件和仪表板服务器。""" 20 | 21 | def __init__(self, db: BaseDatabase, log_broker: LogBroker): 22 | self.db = db 23 | self.logger = logger 24 | self.log_broker = log_broker 25 | 26 | async def start(self): 27 | core_lifecycle = AstrBotCoreLifecycle(self.log_broker, self.db) 28 | 29 | try: 30 | await core_lifecycle.initialize() 31 | except Exception as e: 32 | logger.critical(traceback.format_exc()) 33 | logger.critical(f"😭 初始化 AstrBot 失败:{e} !!!") 34 | return 35 | 36 | core_task = core_lifecycle.start() 37 | 38 | self.dashboard_server = AstrBotDashboard( 39 | core_lifecycle, self.db, core_lifecycle.dashboard_shutdown_event 40 | ) 41 | task = asyncio.gather( 42 | core_task, self.dashboard_server.run() 43 | ) # 启动核心任务和仪表板服务器 44 | 45 | try: 46 | await task # 整个AstrBot在这里运行 47 | except asyncio.CancelledError: 48 | logger.info("🌈 正在关闭 AstrBot...") 49 | await core_lifecycle.stop() 50 | -------------------------------------------------------------------------------- /astrbot/core/pipeline/__init__.py: -------------------------------------------------------------------------------- 1 | from astrbot.core.message.message_event_result import ( 2 | MessageEventResult, 3 | EventResultType, 4 | ) 5 | 6 | from .waking_check.stage import WakingCheckStage 7 | from .whitelist_check.stage import WhitelistCheckStage 8 | from .rate_limit_check.stage import RateLimitStage 9 | from .content_safety_check.stage import ContentSafetyCheckStage 10 | from .platform_compatibility.stage import PlatformCompatibilityStage 11 | from .preprocess_stage.stage import PreProcessStage 12 | from .process_stage.stage import ProcessStage 13 | from .result_decorate.stage import ResultDecorateStage 14 | from .respond.stage import RespondStage 15 | 16 | # 管道阶段顺序 17 | STAGES_ORDER = [ 18 | "WakingCheckStage", # 检查是否需要唤醒 19 | "WhitelistCheckStage", # 检查是否在群聊/私聊白名单 20 | "RateLimitStage", # 检查会话是否超过频率限制 21 | "ContentSafetyCheckStage", # 检查内容安全 22 | "PlatformCompatibilityStage", # 检查所有处理器的平台兼容性 23 | "PreProcessStage", # 预处理 24 | "ProcessStage", # 交由 Stars 处理(a.k.a 插件),或者 LLM 调用 25 | "ResultDecorateStage", # 处理结果,比如添加回复前缀、t2i、转换为语音 等 26 | "RespondStage", # 发送消息 27 | ] 28 | 29 | __all__ = [ 30 | "WakingCheckStage", 31 | "WhitelistCheckStage", 32 | "RateLimitStage", 33 | "ContentSafetyCheckStage", 34 | "PlatformCompatibilityStage", 35 | "PreProcessStage", 36 | "ProcessStage", 37 | "ResultDecorateStage", 38 | "RespondStage", 39 | "MessageEventResult", 40 | "EventResultType", 41 | ] 42 | -------------------------------------------------------------------------------- /astrbot/core/pipeline/content_safety_check/stage.py: -------------------------------------------------------------------------------- 1 | from typing import Union, AsyncGenerator 2 | from ..stage import Stage, register_stage 3 | from ..context import PipelineContext 4 | from astrbot.core.platform.astr_message_event import AstrMessageEvent 5 | from astrbot.core.message.message_event_result import MessageEventResult 6 | from astrbot.core import logger 7 | from .strategies.strategy import StrategySelector 8 | 9 | 10 | @register_stage 11 | class ContentSafetyCheckStage(Stage): 12 | """检查内容安全 13 | 14 | 当前只会检查文本的。 15 | """ 16 | 17 | async def initialize(self, ctx: PipelineContext): 18 | config = ctx.astrbot_config["content_safety"] 19 | self.strategy_selector = StrategySelector(config) 20 | 21 | async def process( 22 | self, event: AstrMessageEvent, check_text: str = None 23 | ) -> Union[None, AsyncGenerator[None, None]]: 24 | """检查内容安全""" 25 | text = check_text if check_text else event.get_message_str() 26 | ok, info = self.strategy_selector.check(text) 27 | if not ok: 28 | if event.is_at_or_wake_command: 29 | event.set_result( 30 | MessageEventResult().message( 31 | "你的消息或者大模型的响应中包含不适当的内容,已被屏蔽。" 32 | ) 33 | ) 34 | yield 35 | event.stop_event() 36 | logger.info(f"内容安全检查不通过,原因:{info}") 37 | return 38 | -------------------------------------------------------------------------------- /astrbot/core/pipeline/content_safety_check/strategies/__init__.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import Tuple 3 | 4 | 5 | class ContentSafetyStrategy(abc.ABC): 6 | @abc.abstractmethod 7 | def check(self, content: str) -> Tuple[bool, str]: 8 | raise NotImplementedError 9 | -------------------------------------------------------------------------------- /astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py: -------------------------------------------------------------------------------- 1 | """ 2 | 使用此功能应该先 pip install baidu-aip 3 | """ 4 | 5 | from . import ContentSafetyStrategy 6 | from aip import AipContentCensor 7 | 8 | 9 | class BaiduAipStrategy(ContentSafetyStrategy): 10 | def __init__(self, appid: str, ak: str, sk: str) -> None: 11 | self.app_id = appid 12 | self.api_key = ak 13 | self.secret_key = sk 14 | self.client = AipContentCensor(self.app_id, self.api_key, self.secret_key) 15 | 16 | def check(self, content: str): 17 | res = self.client.textCensorUserDefined(content) 18 | if "conclusionType" not in res: 19 | return False, "" 20 | if res["conclusionType"] == 1: 21 | return True, "" 22 | else: 23 | if "data" not in res: 24 | return False, "" 25 | count = len(res["data"]) 26 | info = f"百度审核服务发现 {count} 处违规:\n" 27 | for i in res["data"]: 28 | info += f"{i['msg']};\n" 29 | info += "\n判断结果:" + res["conclusion"] 30 | return False, info 31 | -------------------------------------------------------------------------------- /astrbot/core/pipeline/content_safety_check/strategies/keywords.py: -------------------------------------------------------------------------------- 1 | import re 2 | from . import ContentSafetyStrategy 3 | 4 | 5 | class KeywordsStrategy(ContentSafetyStrategy): 6 | def __init__(self, extra_keywords: list) -> None: 7 | self.keywords = [] 8 | if extra_keywords is None: 9 | extra_keywords = [] 10 | self.keywords.extend(extra_keywords) 11 | # keywords_path = os.path.join(os.path.dirname(__file__), "unfit_words") 12 | # internal keywords 13 | # if os.path.exists(keywords_path): 14 | # with open(keywords_path, "r", encoding="utf-8") as f: 15 | # self.keywords.extend( 16 | # json.loads(base64.b64decode(f.read()).decode("utf-8"))["keywords"] 17 | # ) 18 | 19 | def check(self, content: str) -> bool: 20 | for keyword in self.keywords: 21 | if re.search(keyword, content): 22 | return False, "内容安全检查不通过,匹配到敏感词。" 23 | return True, "" 24 | -------------------------------------------------------------------------------- /astrbot/core/pipeline/content_safety_check/strategies/strategy.py: -------------------------------------------------------------------------------- 1 | from . import ContentSafetyStrategy 2 | from typing import List, Tuple 3 | from astrbot import logger 4 | 5 | 6 | class StrategySelector: 7 | def __init__(self, config: dict) -> None: 8 | self.enabled_strategies: List[ContentSafetyStrategy] = [] 9 | if config["internal_keywords"]["enable"]: 10 | from .keywords import KeywordsStrategy 11 | 12 | self.enabled_strategies.append( 13 | KeywordsStrategy(config["internal_keywords"]["extra_keywords"]) 14 | ) 15 | if config["baidu_aip"]["enable"]: 16 | try: 17 | from .baidu_aip import BaiduAipStrategy 18 | except ImportError: 19 | logger.warning("使用百度内容审核应该先 pip install baidu-aip") 20 | return 21 | self.enabled_strategies.append( 22 | BaiduAipStrategy( 23 | config["baidu_aip"]["app_id"], 24 | config["baidu_aip"]["api_key"], 25 | config["baidu_aip"]["secret_key"], 26 | ) 27 | ) 28 | 29 | def check(self, content: str) -> Tuple[bool, str]: 30 | for strategy in self.enabled_strategies: 31 | ok, info = strategy.check(content) 32 | if not ok: 33 | return False, info 34 | return True, "" 35 | -------------------------------------------------------------------------------- /astrbot/core/pipeline/context.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from astrbot.core.config.astrbot_config import AstrBotConfig 3 | from astrbot.core.star import PluginManager 4 | 5 | 6 | @dataclass 7 | class PipelineContext: 8 | """上下文对象,包含管道执行所需的上下文信息""" 9 | 10 | astrbot_config: AstrBotConfig # AstrBot 配置对象 11 | plugin_manager: PluginManager # 插件管理器对象 12 | -------------------------------------------------------------------------------- /astrbot/core/pipeline/platform_compatibility/stage.py: -------------------------------------------------------------------------------- 1 | from ..stage import Stage, register_stage 2 | from ..context import PipelineContext 3 | from typing import Union, AsyncGenerator 4 | from astrbot.core.platform.astr_message_event import AstrMessageEvent 5 | from astrbot.core.star.star import star_map 6 | from astrbot.core.star.star_handler import StarHandlerMetadata 7 | from astrbot.core import logger 8 | 9 | 10 | @register_stage 11 | class PlatformCompatibilityStage(Stage): 12 | """检查所有处理器的平台兼容性。 13 | 14 | 这个阶段会检查所有处理器是否在当前平台启用,如果未启用则设置platform_compatible属性为False。 15 | """ 16 | 17 | async def initialize(self, ctx: PipelineContext) -> None: 18 | """初始化平台兼容性检查阶段 19 | 20 | Args: 21 | ctx (PipelineContext): 消息管道上下文对象, 包括配置和插件管理器 22 | """ 23 | self.ctx = ctx 24 | 25 | async def process( 26 | self, event: AstrMessageEvent 27 | ) -> Union[None, AsyncGenerator[None, None]]: 28 | # 获取当前平台ID 29 | platform_id = event.get_platform_id() 30 | 31 | # 获取已激活的处理器 32 | activated_handlers = event.get_extra("activated_handlers") 33 | if activated_handlers is None: 34 | activated_handlers = [] 35 | 36 | # 标记不兼容的处理器 37 | for handler in activated_handlers: 38 | if not isinstance(handler, StarHandlerMetadata): 39 | continue 40 | # 检查处理器是否在当前平台启用 41 | enabled = handler.is_enabled_for_platform(platform_id) 42 | if not enabled: 43 | if handler.handler_module_path in star_map: 44 | plugin_name = star_map[handler.handler_module_path].name 45 | logger.debug( 46 | f"[PlatformCompatibilityStage] 插件 {plugin_name} 在平台 {platform_id} 未启用,标记处理器 {handler.handler_name} 为平台不兼容" 47 | ) 48 | # 设置处理器为平台不兼容状态 49 | # TODO: 更好的标记方式 50 | handler.platform_compatible = False 51 | else: 52 | # 确保处理器为平台兼容状态 53 | handler.platform_compatible = True 54 | 55 | # 更新已激活的处理器列表 56 | event.set_extra("activated_handlers", activated_handlers) 57 | -------------------------------------------------------------------------------- /astrbot/core/pipeline/whitelist_check/stage.py: -------------------------------------------------------------------------------- 1 | from ..stage import Stage, register_stage 2 | from ..context import PipelineContext 3 | from typing import AsyncGenerator, Union 4 | from astrbot.core.platform.astr_message_event import AstrMessageEvent 5 | from astrbot.core.platform.message_type import MessageType 6 | from astrbot.core import logger 7 | 8 | 9 | @register_stage 10 | class WhitelistCheckStage(Stage): 11 | """检查是否在群聊/私聊白名单""" 12 | 13 | async def initialize(self, ctx: PipelineContext) -> None: 14 | self.enable_whitelist_check = ctx.astrbot_config["platform_settings"][ 15 | "enable_id_white_list" 16 | ] 17 | self.whitelist = ctx.astrbot_config["platform_settings"]["id_whitelist"] 18 | self.whitelist = [ 19 | str(i).strip() for i in self.whitelist if str(i).strip() != "" 20 | ] 21 | self.wl_ignore_admin_on_group = ctx.astrbot_config["platform_settings"][ 22 | "wl_ignore_admin_on_group" 23 | ] 24 | self.wl_ignore_admin_on_friend = ctx.astrbot_config["platform_settings"][ 25 | "wl_ignore_admin_on_friend" 26 | ] 27 | self.wl_log = ctx.astrbot_config["platform_settings"]["id_whitelist_log"] 28 | 29 | async def process( 30 | self, event: AstrMessageEvent 31 | ) -> Union[None, AsyncGenerator[None, None]]: 32 | if not self.enable_whitelist_check: 33 | # 白名单检查未启用 34 | return 35 | 36 | if len(self.whitelist) == 0: 37 | # 白名单为空,不检查 38 | return 39 | 40 | if event.get_platform_name() == "webchat": 41 | # WebChat 豁免 42 | return 43 | 44 | # 检查是否在白名单 45 | if self.wl_ignore_admin_on_group: 46 | if ( 47 | event.role == "admin" 48 | and event.get_message_type() == MessageType.GROUP_MESSAGE 49 | ): 50 | return 51 | if self.wl_ignore_admin_on_friend: 52 | if ( 53 | event.role == "admin" 54 | and event.get_message_type() == MessageType.FRIEND_MESSAGE 55 | ): 56 | return 57 | if ( 58 | event.unified_msg_origin not in self.whitelist 59 | and str(event.get_group_id()).strip() not in self.whitelist 60 | ): 61 | if self.wl_log: 62 | logger.info( 63 | f"会话 ID {event.unified_msg_origin} 不在会话白名单中,已终止事件传播。请在配置文件中添加该会话 ID 到白名单。" 64 | ) 65 | event.stop_event() 66 | -------------------------------------------------------------------------------- /astrbot/core/platform/__init__.py: -------------------------------------------------------------------------------- 1 | from .platform import Platform 2 | from .astr_message_event import AstrMessageEvent 3 | from .platform_metadata import PlatformMetadata 4 | from .astrbot_message import AstrBotMessage, MessageMember, MessageType, Group 5 | 6 | __all__ = [ 7 | "Platform", 8 | "AstrMessageEvent", 9 | "PlatformMetadata", 10 | "AstrBotMessage", 11 | "MessageMember", 12 | "MessageType", 13 | "Group", 14 | ] 15 | -------------------------------------------------------------------------------- /astrbot/core/platform/astrbot_message.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import List 3 | from dataclasses import dataclass 4 | from astrbot.core.message.components import BaseMessageComponent 5 | from .message_type import MessageType 6 | 7 | 8 | @dataclass 9 | class MessageMember: 10 | user_id: str # 发送者id 11 | nickname: str = None 12 | 13 | def __str__(self): 14 | # 使用 f-string 来构建返回的字符串表示形式 15 | return ( 16 | f"User ID: {self.user_id}," 17 | f"Nickname: {self.nickname if self.nickname else 'N/A'}" 18 | ) 19 | 20 | 21 | @dataclass 22 | class Group: 23 | group_id: str 24 | """群号""" 25 | group_name: str = None 26 | """群名称""" 27 | group_avatar: str = None 28 | """群头像""" 29 | group_owner: str = None 30 | """群主 id""" 31 | group_admins: List[str] = None 32 | """群管理员 id""" 33 | members: List[MessageMember] = None 34 | """所有群成员""" 35 | 36 | def __str__(self): 37 | # 使用 f-string 来构建返回的字符串表示形式 38 | return ( 39 | f"Group ID: {self.group_id}\n" 40 | f"Name: {self.group_name if self.group_name else 'N/A'}\n" 41 | f"Avatar: {self.group_avatar if self.group_avatar else 'N/A'}\n" 42 | f"Owner ID: {self.group_owner if self.group_owner else 'N/A'}\n" 43 | f"Admin IDs: {self.group_admins if self.group_admins else 'N/A'}\n" 44 | f"Members Len: {len(self.members) if self.members else 0}\n" 45 | f"First Member: {self.members[0] if self.members else 'N/A'}\n" 46 | ) 47 | 48 | 49 | class AstrBotMessage: 50 | """ 51 | AstrBot 的消息对象 52 | """ 53 | 54 | type: MessageType # 消息类型 55 | self_id: str # 机器人的识别id 56 | session_id: str # 会话id。取决于 unique_session 的设置。 57 | message_id: str # 消息id 58 | group_id: str = "" # 群组id,如果为私聊,则为空 59 | sender: MessageMember # 发送者 60 | message: List[BaseMessageComponent] # 消息链使用 Nakuru 的消息链格式 61 | message_str: str # 最直观的纯文本消息字符串 62 | raw_message: object 63 | timestamp: int # 消息时间戳 64 | 65 | def __init__(self) -> None: 66 | self.timestamp = int(time.time()) 67 | 68 | def __str__(self) -> str: 69 | return str(self.__dict__) 70 | -------------------------------------------------------------------------------- /astrbot/core/platform/message_type.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class MessageType(Enum): 5 | GROUP_MESSAGE = "GroupMessage" # 群组形式的消息 6 | FRIEND_MESSAGE = "FriendMessage" # 私聊、好友等单聊消息 7 | OTHER_MESSAGE = "OtherMessage" # 其他类型的消息,如系统消息等 8 | -------------------------------------------------------------------------------- /astrbot/core/platform/platform.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import uuid 3 | from typing import Awaitable, Any 4 | from asyncio import Queue 5 | from .platform_metadata import PlatformMetadata 6 | from .astr_message_event import AstrMessageEvent 7 | from astrbot.core.message.message_event_result import MessageChain 8 | from .astr_message_event import MessageSesion 9 | from astrbot.core.utils.metrics import Metric 10 | 11 | 12 | class Platform(abc.ABC): 13 | def __init__(self, event_queue: Queue): 14 | super().__init__() 15 | # 维护了消息平台的事件队列,EventBus 会从这里取出事件并处理。 16 | self._event_queue = event_queue 17 | self.client_self_id = uuid.uuid4().hex 18 | 19 | @abc.abstractmethod 20 | def run(self) -> Awaitable[Any]: 21 | """ 22 | 得到一个平台的运行实例,需要返回一个协程对象。 23 | """ 24 | raise NotImplementedError 25 | 26 | async def terminate(self): 27 | """ 28 | 终止一个平台的运行实例。 29 | """ 30 | ... 31 | 32 | @abc.abstractmethod 33 | def meta(self) -> PlatformMetadata: 34 | """ 35 | 得到一个平台的元数据。 36 | """ 37 | raise NotImplementedError 38 | 39 | async def send_by_session( 40 | self, session: MessageSesion, message_chain: MessageChain 41 | ) -> Awaitable[Any]: 42 | """ 43 | 通过会话发送消息。该方法旨在让插件能够直接通过**可持久化的会话数据**发送消息,而不需要保存 event 对象。 44 | 45 | 异步方法。 46 | """ 47 | await Metric.upload(msg_event_tick=1, adapter_name=self.meta().name) 48 | 49 | def commit_event(self, event: AstrMessageEvent): 50 | """ 51 | 提交一个事件到事件队列。 52 | """ 53 | self._event_queue.put_nowait(event) 54 | 55 | def get_client(self): 56 | """ 57 | 获取平台的客户端对象。 58 | """ 59 | pass 60 | -------------------------------------------------------------------------------- /astrbot/core/platform/platform_metadata.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class PlatformMetadata: 6 | name: str 7 | """平台的名称""" 8 | description: str 9 | """平台的描述""" 10 | id: str = None 11 | """平台的唯一标识符,用于配置中识别特定平台""" 12 | 13 | default_config_tmpl: dict = None 14 | """平台的默认配置模板""" 15 | adapter_display_name: str = None 16 | """显示在 WebUI 配置页中的平台名称,如空则是 name""" 17 | -------------------------------------------------------------------------------- /astrbot/core/platform/register.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Type 2 | from .platform_metadata import PlatformMetadata 3 | from astrbot.core import logger 4 | 5 | platform_registry: List[PlatformMetadata] = [] 6 | """维护了通过装饰器注册的平台适配器""" 7 | platform_cls_map: Dict[str, Type] = {} 8 | """维护了平台适配器名称和适配器类的映射""" 9 | 10 | 11 | def register_platform_adapter( 12 | adapter_name: str, 13 | desc: str, 14 | default_config_tmpl: dict = None, 15 | adapter_display_name: str = None, 16 | ): 17 | """用于注册平台适配器的带参装饰器。 18 | 19 | default_config_tmpl 指定了平台适配器的默认配置模板。用户填写好后将会作为 platform_config 传入你的 Platform 类的实现类。 20 | """ 21 | 22 | def decorator(cls): 23 | if adapter_name in platform_cls_map: 24 | raise ValueError( 25 | f"平台适配器 {adapter_name} 已经注册过了,可能发生了适配器命名冲突。" 26 | ) 27 | 28 | # 添加必备选项 29 | if default_config_tmpl: 30 | if "type" not in default_config_tmpl: 31 | default_config_tmpl["type"] = adapter_name 32 | if "enable" not in default_config_tmpl: 33 | default_config_tmpl["enable"] = False 34 | if "id" not in default_config_tmpl: 35 | default_config_tmpl["id"] = adapter_name 36 | 37 | pm = PlatformMetadata( 38 | name=adapter_name, 39 | description=desc, 40 | default_config_tmpl=default_config_tmpl, 41 | adapter_display_name=adapter_display_name, 42 | ) 43 | platform_registry.append(pm) 44 | platform_cls_map[adapter_name] = cls 45 | logger.debug(f"平台适配器 {adapter_name} 已注册") 46 | return cls 47 | 48 | return decorator 49 | -------------------------------------------------------------------------------- /astrbot/core/platform/sources/gewechat/downloader.py: -------------------------------------------------------------------------------- 1 | from astrbot import logger 2 | import aiohttp 3 | import json 4 | 5 | 6 | class GeweDownloader: 7 | def __init__(self, base_url: str, download_base_url: str, token: str): 8 | self.base_url = base_url 9 | self.download_base_url = download_base_url 10 | self.headers = {"Content-Type": "application/json", "X-GEWE-TOKEN": token} 11 | 12 | async def _post_json(self, baseurl: str, route: str, payload: dict): 13 | async with aiohttp.ClientSession() as session: 14 | async with session.post( 15 | f"{baseurl}{route}", headers=self.headers, json=payload 16 | ) as resp: 17 | return await resp.read() 18 | 19 | async def download_voice(self, appid: str, xml: str, msg_id: str): 20 | payload = {"appId": appid, "xml": xml, "msgId": msg_id} 21 | return await self._post_json(self.base_url, "/message/downloadVoice", payload) 22 | 23 | async def download_image(self, appid: str, xml: str) -> str: 24 | """返回一个可下载的 URL""" 25 | choices = [2, 3] # 2:常规图片 3:缩略图 26 | 27 | for choice in choices: 28 | try: 29 | payload = {"appId": appid, "xml": xml, "type": choice} 30 | data = await self._post_json( 31 | self.base_url, "/message/downloadImage", payload 32 | ) 33 | json_blob = json.loads(data) 34 | if "fileUrl" in json_blob["data"]: 35 | return self.download_base_url + json_blob["data"]["fileUrl"] 36 | 37 | except BaseException as e: 38 | logger.error(f"gewe download image: {e}") 39 | continue 40 | 41 | raise Exception("无法下载图片") 42 | 43 | async def download_emoji_md5(self, app_id, emoji_md5): 44 | """下载emoji""" 45 | try: 46 | payload = {"appId": app_id, "emojiMd5": emoji_md5} 47 | 48 | # gewe 计划中的接口,暂时没有实现。返回代码404 49 | data = await self._post_json( 50 | self.base_url, "/message/downloadEmojiMd5", payload 51 | ) 52 | json_blob = json.loads(data) 53 | return json_blob 54 | except BaseException as e: 55 | logger.error(f"gewe download emoji: {e}") 56 | -------------------------------------------------------------------------------- /astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_event.py: -------------------------------------------------------------------------------- 1 | from astrbot.api.platform import AstrBotMessage, PlatformMetadata 2 | from botpy import Client 3 | from ..qqofficial.qqofficial_message_event import QQOfficialMessageEvent 4 | 5 | 6 | class QQOfficialWebhookMessageEvent(QQOfficialMessageEvent): 7 | def __init__( 8 | self, 9 | message_str: str, 10 | message_obj: AstrBotMessage, 11 | platform_meta: PlatformMetadata, 12 | session_id: str, 13 | bot: Client, 14 | ): 15 | super().__init__(message_str, message_obj, platform_meta, session_id, bot) 16 | -------------------------------------------------------------------------------- /astrbot/core/provider/__init__.py: -------------------------------------------------------------------------------- 1 | from .provider import Provider, Personality, STTProvider 2 | 3 | from .entities import ProviderMetaData 4 | 5 | __all__ = ["Provider", "Personality", "ProviderMetaData", "STTProvider"] 6 | -------------------------------------------------------------------------------- /astrbot/core/provider/entites.py: -------------------------------------------------------------------------------- 1 | from astrbot.core.provider.entities import ( 2 | ProviderRequest, 3 | ProviderType, 4 | ProviderMetaData, 5 | ToolCallsResult, 6 | AssistantMessageSegment, 7 | ToolCallMessageSegment, 8 | LLMResponse, 9 | ) 10 | 11 | __all__ = [ 12 | "ProviderRequest", 13 | "ProviderType", 14 | "ProviderMetaData", 15 | "ToolCallsResult", 16 | "AssistantMessageSegment", 17 | "ToolCallMessageSegment", 18 | "LLMResponse", 19 | ] 20 | -------------------------------------------------------------------------------- /astrbot/core/provider/register.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict 2 | from .entities import ProviderMetaData, ProviderType 3 | from astrbot.core import logger 4 | from .func_tool_manager import FuncCall 5 | 6 | provider_registry: List[ProviderMetaData] = [] 7 | """维护了通过装饰器注册的 Provider""" 8 | provider_cls_map: Dict[str, ProviderMetaData] = {} 9 | """维护了 Provider 类型名称和 ProviderMetadata 的映射""" 10 | 11 | llm_tools = FuncCall() 12 | 13 | 14 | def register_provider_adapter( 15 | provider_type_name: str, 16 | desc: str, 17 | provider_type: ProviderType = ProviderType.CHAT_COMPLETION, 18 | default_config_tmpl: dict = None, 19 | provider_display_name: str = None, 20 | ): 21 | """用于注册平台适配器的带参装饰器""" 22 | 23 | def decorator(cls): 24 | if provider_type_name in provider_cls_map: 25 | raise ValueError( 26 | f"检测到大模型提供商适配器 {provider_type_name} 已经注册,可能发生了大模型提供商适配器类型命名冲突。" 27 | ) 28 | 29 | # 添加必备选项 30 | if default_config_tmpl: 31 | if "type" not in default_config_tmpl: 32 | default_config_tmpl["type"] = provider_type_name 33 | if "enable" not in default_config_tmpl: 34 | default_config_tmpl["enable"] = False 35 | if "id" not in default_config_tmpl: 36 | default_config_tmpl["id"] = provider_type_name 37 | 38 | pm = ProviderMetaData( 39 | type=provider_type_name, 40 | desc=desc, 41 | provider_type=provider_type, 42 | cls_type=cls, 43 | default_config_tmpl=default_config_tmpl, 44 | provider_display_name=provider_display_name, 45 | ) 46 | provider_registry.append(pm) 47 | provider_cls_map[provider_type_name] = pm 48 | logger.debug(f"服务提供商 Provider {provider_type_name} 已注册") 49 | return cls 50 | 51 | return decorator 52 | -------------------------------------------------------------------------------- /astrbot/core/provider/sources/dashscope_tts.py: -------------------------------------------------------------------------------- 1 | import os 2 | import dashscope 3 | import uuid 4 | import asyncio 5 | from dashscope.audio.tts_v2 import * 6 | from ..provider import TTSProvider 7 | from ..entities import ProviderType 8 | from ..register import register_provider_adapter 9 | from astrbot.core.utils.astrbot_path import get_astrbot_data_path 10 | 11 | 12 | @register_provider_adapter( 13 | "dashscope_tts", "Dashscope TTS API", provider_type=ProviderType.TEXT_TO_SPEECH 14 | ) 15 | class ProviderDashscopeTTSAPI(TTSProvider): 16 | def __init__( 17 | self, 18 | provider_config: dict, 19 | provider_settings: dict, 20 | ) -> None: 21 | super().__init__(provider_config, provider_settings) 22 | self.chosen_api_key: str = provider_config.get("api_key", "") 23 | self.voice: str = provider_config.get("dashscope_tts_voice", "loongstella") 24 | self.set_model(provider_config.get("model", None)) 25 | self.timeout_ms = float(provider_config.get("timeout", 20)) * 1000 26 | dashscope.api_key = self.chosen_api_key 27 | 28 | async def get_audio(self, text: str) -> str: 29 | temp_dir = os.path.join(get_astrbot_data_path(), "temp") 30 | path = os.path.join(temp_dir, f"dashscope_tts_{uuid.uuid4()}.wav") 31 | self.synthesizer = SpeechSynthesizer( 32 | model=self.get_model(), 33 | voice=self.voice, 34 | format=AudioFormat.WAV_24000HZ_MONO_16BIT, 35 | ) 36 | audio = await asyncio.get_event_loop().run_in_executor( 37 | None, self.synthesizer.call, text, self.timeout_ms 38 | ) 39 | with open(path, "wb") as f: 40 | f.write(audio) 41 | return path 42 | -------------------------------------------------------------------------------- /astrbot/core/provider/sources/gemini_embedding_source.py: -------------------------------------------------------------------------------- 1 | from google import genai 2 | from google.genai import types 3 | from google.genai.errors import APIError 4 | from ..provider import EmbeddingProvider 5 | from ..register import register_provider_adapter 6 | from ..entities import ProviderType 7 | 8 | 9 | @register_provider_adapter( 10 | "gemini_embedding", 11 | "Google Gemini Embedding 提供商适配器", 12 | provider_type=ProviderType.EMBEDDING, 13 | ) 14 | class GeminiEmbeddingProvider(EmbeddingProvider): 15 | def __init__(self, provider_config: dict, provider_settings: dict) -> None: 16 | super().__init__(provider_config, provider_settings) 17 | self.provider_config = provider_config 18 | self.provider_settings = provider_settings 19 | 20 | api_key: str = provider_config.get("embedding_api_key") 21 | api_base: str = provider_config.get("embedding_api_base", None) 22 | timeout: int = int(provider_config.get("timeout", 20)) 23 | 24 | http_options = types.HttpOptions(timeout=timeout * 1000) 25 | if api_base: 26 | if api_base.endswith("/"): 27 | api_base = api_base[:-1] 28 | http_options.base_url = api_base 29 | 30 | self.client = genai.Client(api_key=api_key, http_options=http_options).aio 31 | 32 | self.model = provider_config.get( 33 | "embedding_model", "gemini-embedding-exp-03-07" 34 | ) 35 | self.dimension = provider_config.get("embedding_dimensions", 768) 36 | 37 | async def get_embedding(self, text: str) -> list[float]: 38 | """ 39 | 获取文本的嵌入 40 | """ 41 | try: 42 | result = await self.client.models.embed_content( 43 | model=self.model, contents=text 44 | ) 45 | return result.embeddings[0].values 46 | except APIError as e: 47 | raise Exception(f"Gemini Embedding API请求失败: {e.message}") 48 | 49 | async def get_embeddings(self, texts: list[str]) -> list[list[float]]: 50 | """ 51 | 批量获取文本的嵌入 52 | """ 53 | try: 54 | result = await self.client.models.embed_content( 55 | model=self.model, contents=texts 56 | ) 57 | return [embedding.values for embedding in result.embeddings] 58 | except APIError as e: 59 | raise Exception(f"Gemini Embedding API批量请求失败: {e.message}") 60 | 61 | def get_dim(self) -> int: 62 | """获取向量的维度""" 63 | return self.dimension 64 | -------------------------------------------------------------------------------- /astrbot/core/provider/sources/gsvi_tts_source.py: -------------------------------------------------------------------------------- 1 | import os 2 | import uuid 3 | import aiohttp 4 | import urllib.parse 5 | from ..provider import TTSProvider 6 | from ..entities import ProviderType 7 | from ..register import register_provider_adapter 8 | from astrbot.core.utils.astrbot_path import get_astrbot_data_path 9 | 10 | 11 | @register_provider_adapter( 12 | "gsvi_tts_api", "GSVI TTS API", provider_type=ProviderType.TEXT_TO_SPEECH 13 | ) 14 | class ProviderGSVITTS(TTSProvider): 15 | def __init__( 16 | self, 17 | provider_config: dict, 18 | provider_settings: dict, 19 | ) -> None: 20 | super().__init__(provider_config, provider_settings) 21 | self.api_base = provider_config.get("api_base", "http://127.0.0.1:5000") 22 | if self.api_base.endswith("/"): 23 | self.api_base = self.api_base[:-1] 24 | self.character = provider_config.get("character") 25 | self.emotion = provider_config.get("emotion") 26 | 27 | async def get_audio(self, text: str) -> str: 28 | temp_dir = os.path.join(get_astrbot_data_path(), "temp") 29 | path = os.path.join(temp_dir, f"gsvi_tts_{uuid.uuid4()}.wav") 30 | params = {"text": text} 31 | 32 | if self.character: 33 | params["character"] = self.character 34 | if self.emotion: 35 | params["emotion"] = self.emotion 36 | 37 | query_parts = [] 38 | for key, value in params.items(): 39 | encoded_value = urllib.parse.quote(str(value)) 40 | query_parts.append(f"{key}={encoded_value}") 41 | 42 | url = f"{self.api_base}/tts?{'&'.join(query_parts)}" 43 | 44 | async with aiohttp.ClientSession() as session: 45 | async with session.get(url) as response: 46 | if response.status == 200: 47 | with open(path, "wb") as f: 48 | f.write(await response.read()) 49 | else: 50 | error_text = await response.text() 51 | raise Exception( 52 | f"GSVI TTS API 请求失败,状态码: {response.status},错误: {error_text}" 53 | ) 54 | 55 | return path 56 | -------------------------------------------------------------------------------- /astrbot/core/provider/sources/openai_embedding_source.py: -------------------------------------------------------------------------------- 1 | from openai import AsyncOpenAI 2 | from ..provider import EmbeddingProvider 3 | from ..register import register_provider_adapter 4 | from ..entities import ProviderType 5 | 6 | 7 | @register_provider_adapter( 8 | "openai_embedding", 9 | "OpenAI API Embedding 提供商适配器", 10 | provider_type=ProviderType.EMBEDDING, 11 | ) 12 | class OpenAIEmbeddingProvider(EmbeddingProvider): 13 | def __init__(self, provider_config: dict, provider_settings: dict) -> None: 14 | super().__init__(provider_config, provider_settings) 15 | self.provider_config = provider_config 16 | self.provider_settings = provider_settings 17 | self.client = AsyncOpenAI( 18 | api_key=provider_config.get("embedding_api_key"), 19 | base_url=provider_config.get( 20 | "embedding_api_base", "https://api.openai.com/v1" 21 | ), 22 | timeout=int(provider_config.get("timeout", 20)), 23 | ) 24 | self.model = provider_config.get("embedding_model", "text-embedding-3-small") 25 | self.dimension = provider_config.get("embedding_dimensions", 1536) 26 | 27 | async def get_embedding(self, text: str) -> list[float]: 28 | """ 29 | 获取文本的嵌入 30 | """ 31 | embedding = await self.client.embeddings.create(input=text, model=self.model) 32 | return embedding.data[0].embedding 33 | 34 | async def get_embeddings(self, texts: list[str]) -> list[list[float]]: 35 | """ 36 | 批量获取文本的嵌入 37 | """ 38 | embeddings = await self.client.embeddings.create(input=texts, model=self.model) 39 | return [item.embedding for item in embeddings.data] 40 | 41 | def get_dim(self) -> int: 42 | """获取向量的维度""" 43 | return self.dimension 44 | -------------------------------------------------------------------------------- /astrbot/core/provider/sources/openai_tts_api_source.py: -------------------------------------------------------------------------------- 1 | import os 2 | import uuid 3 | from openai import AsyncOpenAI, NOT_GIVEN 4 | from ..provider import TTSProvider 5 | from ..entities import ProviderType 6 | from ..register import register_provider_adapter 7 | from astrbot.core.utils.astrbot_path import get_astrbot_data_path 8 | 9 | 10 | @register_provider_adapter( 11 | "openai_tts_api", "OpenAI TTS API", provider_type=ProviderType.TEXT_TO_SPEECH 12 | ) 13 | class ProviderOpenAITTSAPI(TTSProvider): 14 | def __init__( 15 | self, 16 | provider_config: dict, 17 | provider_settings: dict, 18 | ) -> None: 19 | super().__init__(provider_config, provider_settings) 20 | self.chosen_api_key = provider_config.get("api_key", "") 21 | self.voice = provider_config.get("openai-tts-voice", "alloy") 22 | 23 | timeout = provider_config.get("timeout", NOT_GIVEN) 24 | if isinstance(timeout, str): 25 | timeout = int(timeout) 26 | 27 | self.client = AsyncOpenAI( 28 | api_key=self.chosen_api_key, 29 | base_url=provider_config.get("api_base", None), 30 | timeout=timeout, 31 | ) 32 | 33 | self.set_model(provider_config.get("model", None)) 34 | 35 | async def get_audio(self, text: str) -> str: 36 | temp_dir = os.path.join(get_astrbot_data_path(), "temp") 37 | path = os.path.join(temp_dir, f"openai_tts_api_{uuid.uuid4()}.wav") 38 | async with self.client.audio.speech.with_streaming_response.create( 39 | model=self.model_name, voice=self.voice, response_format="wav", input=text 40 | ) as response: 41 | with open(path, "wb") as f: 42 | async for chunk in response.iter_bytes(chunk_size=1024): 43 | f.write(chunk) 44 | return path 45 | -------------------------------------------------------------------------------- /astrbot/core/star/README.md: -------------------------------------------------------------------------------- 1 | # AstrBot Star 2 | 3 | `AstrBot Star` 就是插件。 4 | 5 | 在 AstrBot v4.0 版本后,AstrBot 内部将插件命名为 `star`。插件的 handler 称作 `star_handler`。 -------------------------------------------------------------------------------- /astrbot/core/star/__init__.py: -------------------------------------------------------------------------------- 1 | from .star import StarMetadata 2 | from .star_manager import PluginManager 3 | from .context import Context 4 | from astrbot.core.provider import Provider 5 | from astrbot.core.utils.command_parser import CommandParserMixin 6 | from astrbot.core import html_renderer 7 | from astrbot.core.star.star_tools import StarTools 8 | 9 | 10 | class Star(CommandParserMixin): 11 | """所有插件(Star)的父类,所有插件都应该继承于这个类""" 12 | 13 | def __init__(self, context: Context): 14 | StarTools.initialize(context) 15 | self.context = context 16 | 17 | async def text_to_image(self, text: str, return_url=True) -> str: 18 | """将文本转换为图片""" 19 | return await html_renderer.render_t2i(text, return_url=return_url) 20 | 21 | async def html_render(self, tmpl: str, data: dict, return_url=True) -> str: 22 | """渲染 HTML""" 23 | return await html_renderer.render_custom_template( 24 | tmpl, data, return_url=return_url 25 | ) 26 | 27 | async def terminate(self): 28 | """当插件被禁用、重载插件时会调用这个方法""" 29 | pass 30 | 31 | 32 | __all__ = ["Star", "StarMetadata", "PluginManager", "Context", "Provider", "StarTools"] 33 | -------------------------------------------------------------------------------- /astrbot/core/star/filter/__init__.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from astrbot.core.platform.message_type import MessageType 3 | from astrbot.core.platform.astr_message_event import AstrMessageEvent 4 | from astrbot.core.config import AstrBotConfig 5 | 6 | 7 | class HandlerFilter(abc.ABC): 8 | @abc.abstractmethod 9 | def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool: 10 | """是否应当被过滤""" 11 | raise NotImplementedError 12 | 13 | 14 | __all__ = ["HandlerFilter", "MessageType", "AstrMessageEvent", "AstrBotConfig"] 15 | -------------------------------------------------------------------------------- /astrbot/core/star/filter/custom_filter.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod, ABCMeta 2 | 3 | from . import HandlerFilter 4 | from astrbot.core.platform.astr_message_event import AstrMessageEvent 5 | from astrbot.core.config import AstrBotConfig 6 | 7 | 8 | class CustomFilterMeta(ABCMeta): 9 | def __and__(cls, other): 10 | if not issubclass(other, CustomFilter): 11 | raise TypeError("Operands must be subclasses of CustomFilter.") 12 | return CustomFilterAnd(cls(), other()) 13 | 14 | def __or__(cls, other): 15 | if not issubclass(other, CustomFilter): 16 | raise TypeError("Operands must be subclasses of CustomFilter.") 17 | return CustomFilterOr(cls(), other()) 18 | 19 | 20 | class CustomFilter(HandlerFilter, metaclass=CustomFilterMeta): 21 | def __init__(self, raise_error: bool = True, **kwargs): 22 | self.raise_error = raise_error 23 | 24 | @abstractmethod 25 | def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool: 26 | """一个用于重写的自定义Filter""" 27 | raise NotImplementedError 28 | 29 | def __or__(self, other): 30 | return CustomFilterOr(self, other) 31 | 32 | def __and__(self, other): 33 | return CustomFilterAnd(self, other) 34 | 35 | 36 | class CustomFilterOr(CustomFilter): 37 | def __init__(self, filter1: CustomFilter, filter2: CustomFilter): 38 | super().__init__() 39 | if not isinstance(filter1, (CustomFilter, CustomFilterAnd, CustomFilterOr)): 40 | raise ValueError( 41 | "CustomFilter lass can only operate with other CustomFilter." 42 | ) 43 | self.filter1 = filter1 44 | self.filter2 = filter2 45 | 46 | def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool: 47 | return self.filter1.filter(event, cfg) or self.filter2.filter(event, cfg) 48 | 49 | 50 | class CustomFilterAnd(CustomFilter): 51 | def __init__(self, filter1: CustomFilter, filter2: CustomFilter): 52 | super().__init__() 53 | if not isinstance(filter1, (CustomFilter, CustomFilterAnd, CustomFilterOr)): 54 | raise ValueError( 55 | "CustomFilter lass can only operate with other CustomFilter." 56 | ) 57 | self.filter1 = filter1 58 | self.filter2 = filter2 59 | 60 | def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool: 61 | return self.filter1.filter(event, cfg) and self.filter2.filter(event, cfg) 62 | -------------------------------------------------------------------------------- /astrbot/core/star/filter/event_message_type.py: -------------------------------------------------------------------------------- 1 | import enum 2 | from . import HandlerFilter 3 | from astrbot.core.platform.astr_message_event import AstrMessageEvent 4 | from astrbot.core.config import AstrBotConfig 5 | from astrbot.core.platform.message_type import MessageType 6 | 7 | 8 | class EventMessageType(enum.Flag): 9 | GROUP_MESSAGE = enum.auto() 10 | PRIVATE_MESSAGE = enum.auto() 11 | OTHER_MESSAGE = enum.auto() 12 | ALL = GROUP_MESSAGE | PRIVATE_MESSAGE | OTHER_MESSAGE 13 | 14 | 15 | MESSAGE_TYPE_2_EVENT_MESSAGE_TYPE = { 16 | MessageType.GROUP_MESSAGE: EventMessageType.GROUP_MESSAGE, 17 | MessageType.FRIEND_MESSAGE: EventMessageType.PRIVATE_MESSAGE, 18 | MessageType.OTHER_MESSAGE: EventMessageType.OTHER_MESSAGE, 19 | } 20 | 21 | 22 | class EventMessageTypeFilter(HandlerFilter): 23 | def __init__(self, event_message_type: EventMessageType): 24 | self.event_message_type = event_message_type 25 | 26 | def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool: 27 | message_type = event.get_message_type() 28 | if message_type in MESSAGE_TYPE_2_EVENT_MESSAGE_TYPE: 29 | event_message_type = MESSAGE_TYPE_2_EVENT_MESSAGE_TYPE[message_type] 30 | return bool(event_message_type & self.event_message_type) 31 | return False 32 | -------------------------------------------------------------------------------- /astrbot/core/star/filter/permission.py: -------------------------------------------------------------------------------- 1 | import enum 2 | from . import HandlerFilter 3 | from astrbot.core.platform.astr_message_event import AstrMessageEvent 4 | from astrbot.core.config import AstrBotConfig 5 | 6 | 7 | class PermissionType(enum.Flag): 8 | """权限类型。当选择 MEMBER,ADMIN 也可以通过。""" 9 | 10 | ADMIN = enum.auto() 11 | MEMBER = enum.auto() 12 | 13 | 14 | class PermissionTypeFilter(HandlerFilter): 15 | def __init__(self, permission_type: PermissionType, raise_error: bool = True): 16 | self.permission_type = permission_type 17 | self.raise_error = raise_error 18 | 19 | def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool: 20 | """过滤器""" 21 | if self.permission_type == PermissionType.ADMIN: 22 | if not event.is_admin(): 23 | # event.stop_event() 24 | # raise ValueError(f"您 (ID: {event.get_sender_id()}) 没有权限操作管理员指令。") 25 | return False 26 | 27 | return True 28 | -------------------------------------------------------------------------------- /astrbot/core/star/filter/platform_adapter_type.py: -------------------------------------------------------------------------------- 1 | import enum 2 | from . import HandlerFilter 3 | from astrbot.core.platform.astr_message_event import AstrMessageEvent 4 | from astrbot.core.config import AstrBotConfig 5 | from typing import Union 6 | 7 | 8 | class PlatformAdapterType(enum.Flag): 9 | AIOCQHTTP = enum.auto() 10 | QQOFFICIAL = enum.auto() 11 | VCHAT = enum.auto() 12 | GEWECHAT = enum.auto() 13 | TELEGRAM = enum.auto() 14 | WECOM = enum.auto() 15 | LARK = enum.auto() 16 | ALL = AIOCQHTTP | QQOFFICIAL | VCHAT | GEWECHAT | TELEGRAM | WECOM | LARK 17 | 18 | 19 | ADAPTER_NAME_2_TYPE = { 20 | "aiocqhttp": PlatformAdapterType.AIOCQHTTP, 21 | "qq_official": PlatformAdapterType.QQOFFICIAL, 22 | "vchat": PlatformAdapterType.VCHAT, 23 | "gewechat": PlatformAdapterType.GEWECHAT, 24 | "telegram": PlatformAdapterType.TELEGRAM, 25 | "wecom": PlatformAdapterType.WECOM, 26 | "lark": PlatformAdapterType.LARK, 27 | } 28 | 29 | 30 | class PlatformAdapterTypeFilter(HandlerFilter): 31 | def __init__(self, platform_adapter_type_or_str: Union[PlatformAdapterType, str]): 32 | self.type_or_str = platform_adapter_type_or_str 33 | 34 | def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool: 35 | adapter_name = event.get_platform_name() 36 | if adapter_name in ADAPTER_NAME_2_TYPE: 37 | return ADAPTER_NAME_2_TYPE[adapter_name] & self.type_or_str 38 | return False 39 | -------------------------------------------------------------------------------- /astrbot/core/star/filter/regex.py: -------------------------------------------------------------------------------- 1 | import re 2 | from . import HandlerFilter 3 | from astrbot.core.platform.astr_message_event import AstrMessageEvent 4 | from astrbot.core.config import AstrBotConfig 5 | 6 | 7 | # 正则表达式过滤器不会受到 wake_prefix 的制约。 8 | class RegexFilter(HandlerFilter): 9 | """正则表达式过滤器""" 10 | 11 | def __init__(self, regex: str): 12 | self.regex_str = regex 13 | self.regex = re.compile(regex) 14 | 15 | def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool: 16 | return bool(self.regex.match(event.get_message_str().strip())) 17 | -------------------------------------------------------------------------------- /astrbot/core/star/register/__init__.py: -------------------------------------------------------------------------------- 1 | from .star import register_star 2 | from .star_handler import ( 3 | register_command, 4 | register_command_group, 5 | register_event_message_type, 6 | register_platform_adapter_type, 7 | register_regex, 8 | register_permission_type, 9 | register_custom_filter, 10 | register_on_astrbot_loaded, 11 | register_on_llm_request, 12 | register_on_llm_response, 13 | register_llm_tool, 14 | register_on_decorating_result, 15 | register_after_message_sent, 16 | ) 17 | 18 | __all__ = [ 19 | "register_star", 20 | "register_command", 21 | "register_command_group", 22 | "register_event_message_type", 23 | "register_platform_adapter_type", 24 | "register_regex", 25 | "register_permission_type", 26 | "register_custom_filter", 27 | "register_on_astrbot_loaded", 28 | "register_on_llm_request", 29 | "register_on_llm_response", 30 | "register_llm_tool", 31 | "register_on_decorating_result", 32 | "register_after_message_sent", 33 | ] 34 | -------------------------------------------------------------------------------- /astrbot/core/star/register/star.py: -------------------------------------------------------------------------------- 1 | from ..star import star_registry, StarMetadata, star_map 2 | 3 | 4 | def register_star(name: str, author: str, desc: str, version: str, repo: str = None): 5 | """注册一个插件(Star)。 6 | 7 | Args: 8 | name: 插件名称。 9 | author: 作者。 10 | desc: 插件的简述。 11 | version: 版本号。 12 | repo: 仓库地址。如果没有填写仓库地址,将无法更新这个插件。 13 | 14 | 如果需要为插件填写帮助信息,请使用如下格式: 15 | 16 | ```python 17 | class MyPlugin(star.Star): 18 | \'\'\'这是帮助信息\'\'\' 19 | ... 20 | 21 | 帮助信息会被自动提取。使用 `/plugin <插件名> 可以查看帮助信息。` 22 | """ 23 | 24 | def decorator(cls): 25 | star_metadata = StarMetadata( 26 | name=name, 27 | author=author, 28 | desc=desc, 29 | version=version, 30 | repo=repo, 31 | star_cls_type=cls, 32 | module_path=cls.__module__, 33 | ) 34 | star_registry.append(star_metadata) 35 | star_map[cls.__module__] = star_metadata 36 | return cls 37 | 38 | return decorator 39 | -------------------------------------------------------------------------------- /astrbot/core/star/star.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from types import ModuleType 4 | from typing import List, Dict 5 | from dataclasses import dataclass, field 6 | from astrbot.core.config import AstrBotConfig 7 | 8 | star_registry: List[StarMetadata] = [] 9 | star_map: Dict[str, StarMetadata] = {} 10 | """key 是模块路径,__module__""" 11 | 12 | 13 | @dataclass 14 | class StarMetadata: 15 | """ 16 | 插件的元数据。 17 | 18 | 当 activated 为 False 时,star_cls 可能为 None,请不要在插件未激活时调用 star_cls 的方法。 19 | """ 20 | 21 | name: str 22 | author: str # 插件作者 23 | desc: str # 插件简介 24 | version: str # 插件版本 25 | repo: str = None # 插件仓库地址 26 | 27 | star_cls_type: type = None 28 | """插件的类对象的类型""" 29 | module_path: str = None 30 | """插件的模块路径""" 31 | 32 | star_cls: object = None 33 | """插件的类对象""" 34 | module: ModuleType = None 35 | """插件的模块对象""" 36 | root_dir_name: str = None 37 | """插件的目录名称""" 38 | reserved: bool = False 39 | """是否是 AstrBot 的保留插件""" 40 | 41 | activated: bool = True 42 | """是否被激活""" 43 | 44 | config: AstrBotConfig = None 45 | """插件配置""" 46 | 47 | star_handler_full_names: List[str] = field(default_factory=list) 48 | """注册的 Handler 的全名列表""" 49 | 50 | supported_platforms: Dict[str, bool] = field(default_factory=dict) 51 | """插件支持的平台ID字典,key为平台ID,value为是否支持""" 52 | 53 | def __str__(self) -> str: 54 | return f"StarMetadata({self.name}, {self.desc}, {self.version}, {self.repo})" 55 | 56 | def update_platform_compatibility(self, plugin_enable_config: dict) -> None: 57 | """更新插件支持的平台列表 58 | 59 | Args: 60 | plugin_enable_config: 平台插件启用配置,即platform_settings.plugin_enable配置项 61 | """ 62 | if not plugin_enable_config: 63 | return 64 | 65 | # 清空之前的配置 66 | self.supported_platforms.clear() 67 | 68 | # 遍历所有平台配置 69 | for platform_id, plugins in plugin_enable_config.items(): 70 | # 检查该插件在当前平台的配置 71 | if self.name in plugins: 72 | self.supported_platforms[platform_id] = plugins[self.name] 73 | else: 74 | # 如果没有明确配置,默认为启用 75 | self.supported_platforms[platform_id] = True 76 | -------------------------------------------------------------------------------- /astrbot/core/utils/astrbot_path.py: -------------------------------------------------------------------------------- 1 | """ 2 | Astrbot统一路径获取 3 | 4 | 项目路径:固定为源码所在路径 5 | 根目录路径:默认为当前工作目录,可通过环境变量 ASTRBOT_ROOT 指定 6 | 数据目录路径:固定为根目录下的 data 目录 7 | 配置文件路径:固定为数据目录下的 config 目录 8 | 插件目录路径:固定为数据目录下的 plugins 目录 9 | """ 10 | 11 | import os 12 | 13 | 14 | def get_astrbot_path() -> str: 15 | """获取Astrbot项目路径""" 16 | return os.path.realpath( 17 | os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../../") 18 | ) 19 | 20 | 21 | def get_astrbot_root() -> str: 22 | """获取Astrbot根目录路径""" 23 | if path := os.environ.get("ASTRBOT_ROOT"): 24 | return os.path.realpath(path) 25 | else: 26 | return os.path.realpath(os.getcwd()) 27 | 28 | 29 | def get_astrbot_data_path() -> str: 30 | """获取Astrbot数据目录路径""" 31 | return os.path.realpath(os.path.join(get_astrbot_root(), "data")) 32 | 33 | 34 | def get_astrbot_config_path() -> str: 35 | """获取Astrbot配置文件路径""" 36 | return os.path.realpath(os.path.join(get_astrbot_data_path(), "config")) 37 | 38 | 39 | def get_astrbot_plugin_path() -> str: 40 | """获取Astrbot插件目录路径""" 41 | return os.path.realpath(os.path.join(get_astrbot_data_path(), "plugins")) 42 | -------------------------------------------------------------------------------- /astrbot/core/utils/command_parser.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | class CommandTokens: 5 | def __init__(self) -> None: 6 | self.tokens = [] 7 | self.len = 0 8 | 9 | def get(self, idx: int): 10 | if idx >= self.len: 11 | return None 12 | return self.tokens[idx].strip() 13 | 14 | 15 | class CommandParserMixin: 16 | def parse_commands(self, message: str): 17 | cmd_tokens = CommandTokens() 18 | cmd_tokens.tokens = re.split(r"\s+", message) 19 | cmd_tokens.len = len(cmd_tokens.tokens) 20 | return cmd_tokens 21 | 22 | def regex_match(self, message: str, command: str) -> bool: 23 | return re.search(command, message, re.MULTILINE) is not None 24 | -------------------------------------------------------------------------------- /astrbot/core/utils/log_pipe.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import os 3 | from logging import Logger 4 | 5 | 6 | class LogPipe(threading.Thread): 7 | def __init__( 8 | self, 9 | level, 10 | logger: Logger, 11 | identifier=None, 12 | callback=None, 13 | ): 14 | threading.Thread.__init__(self) 15 | self.daemon = True 16 | self.level = level 17 | self.fd_read, self.fd_write = os.pipe() 18 | self.identifier = identifier 19 | self.logger = logger 20 | self.callback = callback 21 | self.reader = os.fdopen(self.fd_read) 22 | self.start() 23 | 24 | def fileno(self): 25 | return self.fd_write 26 | 27 | def run(self): 28 | for line in iter(self.reader.readline, ""): 29 | if self.callback: 30 | self.callback(line.strip()) 31 | self.logger.log(self.level, f"[{self.identifier}] {line.strip()}") 32 | 33 | self.reader.close() 34 | 35 | def close(self): 36 | os.close(self.fd_write) 37 | -------------------------------------------------------------------------------- /astrbot/core/utils/metrics.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import sys 3 | import os 4 | import socket 5 | import uuid 6 | from astrbot.core.config import VERSION 7 | from astrbot.core import db_helper, logger 8 | 9 | 10 | class Metric: 11 | _iid_cache = None 12 | 13 | @staticmethod 14 | def get_installation_id(): 15 | """获取或创建一个唯一的安装ID""" 16 | if Metric._iid_cache is not None: 17 | return Metric._iid_cache 18 | 19 | config_dir = os.path.join(os.path.expanduser("~"), ".astrbot") 20 | id_file = os.path.join(config_dir, ".installation_id") 21 | 22 | if os.path.exists(id_file): 23 | try: 24 | with open(id_file, "r") as f: 25 | Metric._iid_cache = f.read().strip() 26 | return Metric._iid_cache 27 | except Exception: 28 | pass 29 | try: 30 | os.makedirs(config_dir, exist_ok=True) 31 | installation_id = str(uuid.uuid4()) 32 | with open(id_file, "w") as f: 33 | f.write(installation_id) 34 | Metric._iid_cache = installation_id 35 | return installation_id 36 | except Exception: 37 | Metric._iid_cache = "null" 38 | return "null" 39 | 40 | @staticmethod 41 | async def upload(**kwargs): 42 | """ 43 | 上传相关非敏感的指标以更好地了解 AstrBot 的使用情况。上传的指标不会包含任何有关消息文本、用户信息等敏感信息。 44 | 45 | Powered by TickStats. 46 | """ 47 | base_url = "https://tickstats.soulter.top/api/metric/90a6c2a1" 48 | kwargs["v"] = VERSION 49 | kwargs["os"] = sys.platform 50 | payload = {"metrics_data": kwargs} 51 | try: 52 | kwargs["hn"] = socket.gethostname() 53 | except Exception: 54 | pass 55 | try: 56 | kwargs["iid"] = Metric.get_installation_id() 57 | except Exception: 58 | pass 59 | try: 60 | if "adapter_name" in kwargs: 61 | db_helper.insert_platform_metrics({kwargs["adapter_name"]: 1}) 62 | if "llm_name" in kwargs: 63 | db_helper.insert_llm_metrics({kwargs["llm_name"]: 1}) 64 | except Exception as e: 65 | logger.error(f"保存指标到数据库失败: {e}") 66 | pass 67 | 68 | try: 69 | async with aiohttp.ClientSession(trust_env=True) as session: 70 | async with session.post(base_url, json=payload, timeout=3) as response: 71 | if response.status != 200: 72 | pass 73 | except Exception: 74 | pass 75 | -------------------------------------------------------------------------------- /astrbot/core/utils/pip_installer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import asyncio 3 | 4 | logger = logging.getLogger("astrbot") 5 | 6 | 7 | class PipInstaller: 8 | def __init__(self, pip_install_arg: str, pypi_index_url: str = None): 9 | self.pip_install_arg = pip_install_arg 10 | self.pypi_index_url = pypi_index_url 11 | 12 | async def install( 13 | self, 14 | package_name: str = None, 15 | requirements_path: str = None, 16 | mirror: str = None, 17 | ): 18 | args = ["install"] 19 | if package_name: 20 | args.append(package_name) 21 | elif requirements_path: 22 | args.extend(["-r", requirements_path]) 23 | 24 | index_url = mirror or self.pypi_index_url or "https://pypi.org/simple" 25 | 26 | args.extend(["--trusted-host", "mirrors.aliyun.com", "-i", index_url]) 27 | 28 | if self.pip_install_arg: 29 | args.extend(self.pip_install_arg.split()) 30 | 31 | logger.info(f"Pip 包管理器: pip {' '.join(args)}") 32 | try: 33 | process = await asyncio.create_subprocess_exec( 34 | "pip", *args, 35 | stdout=asyncio.subprocess.PIPE, 36 | stderr=asyncio.subprocess.STDOUT, 37 | ) 38 | 39 | assert process.stdout is not None 40 | async for line in process.stdout: 41 | logger.info(line.decode().strip()) 42 | 43 | await process.wait() 44 | 45 | if process.returncode != 0: 46 | raise Exception(f"安装失败,错误码:{process.returncode}") 47 | except FileNotFoundError: 48 | # 没有 pip 49 | from pip import main as pip_main 50 | result_code = await asyncio.to_thread(pip_main, args) 51 | 52 | # 清除 pip.main 导致的多余的 logging handlers 53 | for handler in logging.root.handlers[:]: 54 | logging.root.removeHandler(handler) 55 | 56 | if result_code != 0: 57 | raise Exception(f"安装失败,错误码:{result_code}") 58 | -------------------------------------------------------------------------------- /astrbot/core/utils/shared_preferences.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from .astrbot_path import get_astrbot_data_path 4 | 5 | 6 | class SharedPreferences: 7 | def __init__(self, path=None): 8 | if path is None: 9 | path = os.path.join(get_astrbot_data_path(), "shared_preferences.json") 10 | self.path = path 11 | self._data = self._load_preferences() 12 | 13 | def _load_preferences(self): 14 | if os.path.exists(self.path): 15 | try: 16 | with open(self.path, "r") as f: 17 | return json.load(f) 18 | except json.JSONDecodeError: 19 | os.remove(self.path) 20 | return {} 21 | 22 | def _save_preferences(self): 23 | with open(self.path, "w") as f: 24 | json.dump(self._data, f, indent=4, ensure_ascii=False) 25 | f.flush() 26 | 27 | def get(self, key, default=None): 28 | return self._data.get(key, default) 29 | 30 | def put(self, key, value): 31 | self._data[key] = value 32 | self._save_preferences() 33 | 34 | def remove(self, key): 35 | if key in self._data: 36 | del self._data[key] 37 | self._save_preferences() 38 | 39 | def clear(self): 40 | self._data.clear() 41 | self._save_preferences() 42 | -------------------------------------------------------------------------------- /astrbot/core/utils/t2i/__init__.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class RenderStrategy(ABC): 5 | @abstractmethod 6 | def render(self, text: str, return_url: bool) -> str: 7 | pass 8 | 9 | @abstractmethod 10 | def render_custom_template( 11 | self, tmpl_str: str, tmpl_data: dict, return_url: bool 12 | ) -> str: 13 | pass 14 | -------------------------------------------------------------------------------- /astrbot/core/utils/t2i/renderer.py: -------------------------------------------------------------------------------- 1 | from .network_strategy import NetworkRenderStrategy 2 | from .local_strategy import LocalRenderStrategy 3 | from astrbot.core.log import LogManager 4 | 5 | logger = LogManager.GetLogger(log_name="astrbot") 6 | 7 | 8 | class HtmlRenderer: 9 | def __init__(self, endpoint_url: str = None): 10 | self.network_strategy = NetworkRenderStrategy(endpoint_url) 11 | self.local_strategy = LocalRenderStrategy() 12 | 13 | def set_network_endpoint(self, endpoint_url: str): 14 | """设置 t2i 的网络端点。""" 15 | logger.info("文本转图像服务接口: " + endpoint_url) 16 | self.network_strategy.set_endpoint(endpoint_url) 17 | 18 | async def render_custom_template( 19 | self, tmpl_str: str, tmpl_data: dict, return_url: bool = False 20 | ): 21 | """使用自定义文转图模板。该方法会通过网络调用 t2i 终结点图文渲染API。 22 | @param tmpl_str: HTML Jinja2 模板。 23 | @param tmpl_data: jinja2 模板数据。 24 | 25 | @return: 图片 URL 或者文件路径,取决于 return_url 参数。 26 | 27 | @example: 参见 https://astrbot.app 插件开发部分。 28 | """ 29 | local = locals() 30 | local.pop("self") 31 | return await self.network_strategy.render_custom_template(**local) 32 | 33 | async def render_t2i( 34 | self, text: str, use_network: bool = True, return_url: bool = False 35 | ): 36 | """使用默认文转图模板。""" 37 | if use_network: 38 | try: 39 | return await self.network_strategy.render(text, return_url=return_url) 40 | except BaseException as e: 41 | logger.error( 42 | f"Failed to render image via AstrBot API: {e}. Falling back to local rendering." 43 | ) 44 | return await self.local_strategy.render(text) 45 | else: 46 | return await self.local_strategy.render(text) 47 | -------------------------------------------------------------------------------- /astrbot/core/utils/tencent_record_helper.py: -------------------------------------------------------------------------------- 1 | import wave 2 | from io import BytesIO 3 | 4 | 5 | async def tencent_silk_to_wav(silk_path: str, output_path: str) -> str: 6 | import pysilk 7 | 8 | with open(silk_path, "rb") as f: 9 | input_data = f.read() 10 | if input_data.startswith(b"\x02"): 11 | input_data = input_data[1:] 12 | input_io = BytesIO(input_data) 13 | output_io = BytesIO() 14 | pysilk.decode(input_io, output_io, 24000) 15 | output_io.seek(0) 16 | with wave.open(output_path, "wb") as wav: 17 | wav.setnchannels(1) 18 | wav.setsampwidth(2) 19 | wav.setframerate(24000) 20 | wav.writeframes(output_io.read()) 21 | 22 | return output_path 23 | 24 | 25 | async def wav_to_tencent_silk(wav_path: str, output_path: str) -> int: 26 | """返回 duration""" 27 | try: 28 | import pilk 29 | except (ImportError, ModuleNotFoundError) as _: 30 | raise Exception( 31 | "pilk 模块未安装,请前往管理面板->控制台->安装pip库 安装 pilk 这个库" 32 | ) 33 | # with wave.open(wav_path, 'rb') as wav: 34 | # wav_data = wav.readframes(wav.getnframes()) 35 | # wav_data = BytesIO(wav_data) 36 | # output_io = BytesIO() 37 | # pysilk.encode(wav_data, output_io, 24000, 24000) 38 | # output_io.seek(0) 39 | 40 | # # 在首字节添加 \x02,去除结尾的\xff\xff 41 | # silk_data = output_io.read() 42 | # silk_data_with_prefix = b'\x02' + silk_data[:-2] 43 | 44 | # # return BytesIO(silk_data_with_prefix) 45 | # with open(output_path, "wb") as f: 46 | # f.write(silk_data_with_prefix) 47 | 48 | # return 0 49 | with wave.open(wav_path, "rb") as wav: 50 | rate = wav.getframerate() 51 | duration = pilk.encode(wav_path, output_path, pcm_rate=rate, tencent=True) 52 | return duration 53 | -------------------------------------------------------------------------------- /astrbot/dashboard/routes/__init__.py: -------------------------------------------------------------------------------- 1 | from .auth import AuthRoute 2 | from .plugin import PluginRoute 3 | from .config import ConfigRoute 4 | from .update import UpdateRoute 5 | from .stat import StatRoute 6 | from .log import LogRoute 7 | from .static_file import StaticFileRoute 8 | from .chat import ChatRoute 9 | from .tools import ToolsRoute # 导入新的ToolsRoute 10 | from .conversation import ConversationRoute 11 | from .file import FileRoute 12 | 13 | 14 | __all__ = [ 15 | "AuthRoute", 16 | "PluginRoute", 17 | "ConfigRoute", 18 | "UpdateRoute", 19 | "StatRoute", 20 | "LogRoute", 21 | "StaticFileRoute", 22 | "ChatRoute", 23 | "ToolsRoute", 24 | "ConversationRoute", 25 | "FileRoute", 26 | ] 27 | -------------------------------------------------------------------------------- /astrbot/dashboard/routes/file.py: -------------------------------------------------------------------------------- 1 | from .route import Route, RouteContext 2 | from astrbot import logger 3 | from quart import abort, send_file 4 | from astrbot.core import file_token_service 5 | 6 | 7 | class FileRoute(Route): 8 | def __init__( 9 | self, 10 | context: RouteContext, 11 | ) -> None: 12 | super().__init__(context) 13 | self.routes = { 14 | "/file/": ("GET", self.serve_file), 15 | } 16 | self.register_routes() 17 | 18 | async def serve_file(self, file_token: str): 19 | try: 20 | file_path = await file_token_service.handle_file(file_token) 21 | return await send_file(file_path) 22 | except (FileNotFoundError, KeyError) as e: 23 | logger.warning(str(e)) 24 | return abort(404) 25 | -------------------------------------------------------------------------------- /astrbot/dashboard/routes/log.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | from quart import make_response 4 | from astrbot.core import logger, LogBroker 5 | from .route import Route, RouteContext 6 | 7 | 8 | class LogRoute(Route): 9 | def __init__(self, context: RouteContext, log_broker: LogBroker) -> None: 10 | super().__init__(context) 11 | self.log_broker = log_broker 12 | self.app.add_url_rule("/api/live-log", view_func=self.log, methods=["GET"]) 13 | 14 | async def log(self): 15 | async def stream(): 16 | queue = None 17 | try: 18 | queue = self.log_broker.register() 19 | while True: 20 | message = await queue.get() 21 | payload = { 22 | "type": "log", 23 | **message, # see astrbot/core/log.py 24 | } 25 | yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n" 26 | await asyncio.sleep(0.07) # 控制发送频率,避免过快 27 | except asyncio.CancelledError: 28 | pass 29 | except BaseException as e: 30 | logger.error(f"Log SSE 连接错误: {e}") 31 | finally: 32 | if queue: 33 | self.log_broker.unregister(queue) 34 | 35 | response = await make_response( 36 | stream(), 37 | { 38 | "Content-Type": "text/event-stream", 39 | "Cache-Control": "no-cache", 40 | "Connection": "keep-alive", 41 | "Transfer-Encoding": "chunked", 42 | }, 43 | ) 44 | response.timeout = None 45 | return response 46 | -------------------------------------------------------------------------------- /astrbot/dashboard/routes/route.py: -------------------------------------------------------------------------------- 1 | from astrbot.core.config.astrbot_config import AstrBotConfig 2 | from dataclasses import dataclass 3 | from quart import Quart 4 | 5 | 6 | @dataclass 7 | class RouteContext: 8 | config: AstrBotConfig 9 | app: Quart 10 | 11 | 12 | class Route: 13 | def __init__(self, context: RouteContext): 14 | self.app = context.app 15 | self.config = context.config 16 | 17 | def register_routes(self): 18 | for route, (method, func) in self.routes.items(): 19 | self.app.add_url_rule(f"/api{route}", view_func=func, methods=[method]) 20 | 21 | 22 | @dataclass 23 | class Response: 24 | status: str = None 25 | message: str = None 26 | data: dict = None 27 | 28 | def error(self, message: str): 29 | self.status = "error" 30 | self.message = message 31 | return self 32 | 33 | def ok(self, data: dict = {}, message: str = None): 34 | self.status = "ok" 35 | self.data = data 36 | self.message = message 37 | return self 38 | -------------------------------------------------------------------------------- /astrbot/dashboard/routes/static_file.py: -------------------------------------------------------------------------------- 1 | from .route import Route, RouteContext 2 | 3 | 4 | class StaticFileRoute(Route): 5 | def __init__(self, context: RouteContext) -> None: 6 | super().__init__(context) 7 | 8 | index_ = [ 9 | "/", 10 | "/auth/login", 11 | "/config", 12 | "/logs", 13 | "/extension", 14 | "/dashboard/default", 15 | "/alkaid", 16 | "/alkaid/knowledge-base", 17 | "/alkaid/long-term-memory", 18 | "/alkaid/other", 19 | "/console", 20 | "/chat", 21 | "/settings", 22 | "/platforms", 23 | "/providers", 24 | "/about", 25 | "/extension-marketplace", 26 | "/conversation", 27 | "/tool-use", 28 | ] 29 | for i in index_: 30 | self.app.add_url_rule(i, view_func=self.index) 31 | 32 | @self.app.errorhandler(404) 33 | async def page_not_found(e): 34 | return "404 Not found。如果你初次使用打开面板发现 404, 请参考文档: https://astrbot.app/faq.html。如果你正在测试回调地址可达性,显示这段文字说明测试成功了。" 35 | 36 | async def index(self): 37 | return await self.app.send_static_file("index.html") 38 | -------------------------------------------------------------------------------- /changelogs/v3.4.0.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | (Pre release) 3 | 1. 使用事件队列和事件总线解耦消息平台适配器与事件处理的逻辑; 4 | 2. 替换为采用装饰器的 handler 注册风格,支持通过消息事件类型、适配器类型、正则表达式、指令名开头、指令组注册 handler (指令或者监听器),不再推荐通过函数的 handler 注册方式。 5 | 3. 解耦了事件处理时的固定逻辑,采用流水线代替。 6 | 4. 解耦了 Provider 的相关处理逻辑。 7 | 5. 解耦了 Platform 相关处理逻辑。 8 | 6. aiocqhttp 适配器支持设置群聊白名单、私聊白名单; 9 | 7. aiocqhttp 适配器将图片转换成 base64 格式上报,而不需要先上传到图床;https://github.com/Soulter/AstrBot/issues/219 10 | 8. qq_official 适配器在群聊/ C2C 场景下以 base64 格式直接上传到 QQ 服务器,而不需要先上传到图床; 11 | 9. 移除了对 nakuru 适配器的支持; 12 | 10. 移除了 update, reboot 等指令; 13 | 11. 支持使用插件仓库镜像源安装插件、更新项目; 14 | 12. 支持接入微信 15 | 13. 移除了内嵌的管理面板构建文件。 16 | 14. 移除了 nakuru-project 库以适应 Pydantic V2(但仍然保留其对 OneBot 数据结构的封装文件),使用 OneBot 连接到 QQ 请使用 aiocqhttp 适配器(仅支持反向 Websockets,即 AstrBot 做 Websockets 服务器端) 17 | 15. 新的文档 18 | 19 | > 不向后兼容配置文件。 -------------------------------------------------------------------------------- /changelogs/v3.4.1.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 1. Fix websearcher 3 | 2. Fix context send_message() 4 | 3. Add reminder llm tool -------------------------------------------------------------------------------- /changelogs/v3.4.10.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | - 修复 LLM 请求报错信息被覆盖的问题,增强 LLM 请求错误处理 #243 4 | - 修复 Napcat 接口更新导致 QQ 图片发送失败的问题 #246 5 | - 修复某些请求不能正确应用代理的问题 6 | - 针对 api_base 的明显提示,修改 ollama 模板的 api_base #247 7 | - 支持登出 gewechat,在webchat等地方使用 `/gewe_logout` 指令,这在微信上显示账号下线但是 gewe 仍显示设备在线时很好用 8 | - 添加gewechat适配器过滤器 9 | - help显示AstrBot和webui版本 10 | - 优化webui和主程序更新的协调 11 | - 下载管理面板时显示提示、下载进度和下载速度 12 | - 管理面板前端更新功能入口移入右上角更新按钮,以便统一管理 #245 -------------------------------------------------------------------------------- /changelogs/v3.4.11.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | - 为平台和提供商适配器添加默认 ID 配置 #248 4 | - 修复appid保存的问题和部分群聊at失效的问题和群聊@的sender username显示异常的问题 5 | - 优化更新项目时重启可能会导致Address already in use的问题 6 | - 各类异步任务报错后的优雅报错输出,而不是只有在退出程序的时候才输出异常日志。 -------------------------------------------------------------------------------- /changelogs/v3.4.12.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | - Gewechat 微信支持图片、语音的收和发 4 | - 支持 OpenAI TTS(文字转语音) 5 | - 支持路径映射,解决 docker 部署时两端文件系统不一致导致的富媒体文件路径不存在问题 6 | - Napcat 下语音消息可能接收异常 -------------------------------------------------------------------------------- /changelogs/v3.4.13.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | - 修复 astrbot_updator 属性缺失与stt_enabled 未初始化 #252 4 | - 支持消息分段回复 -------------------------------------------------------------------------------- /changelogs/v3.4.14.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | - 修复: TTS 问题 4 | - 新增: **支持记录非唤醒状态下群聊历史记录(beta)** 5 | - 优化: 自动删除 deepseek-r1 模型自带的 think 标签 6 | - 优化: 自动移除 ollama 不支持 tool 的模型的 tool 请求 7 | - 优化: /t2i 即时生效 8 | - 优化: gewechat 消息下发异常处理 -------------------------------------------------------------------------------- /changelogs/v3.4.15.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | - 修复: 配置 Validator 不起效的问题 4 | - 修复: DeepSeek-R1 思考标签问题 5 | - 修复: 分段回复间隔时间不生效 6 | - 修复: 修复白名单为空时依然终止事件 #259 7 | - 修复: 群聊增强某些参数的类型转换问题 8 | - 新增: 插件支持注册配置,详见 [注册插件配置](https://astrbot.app/dev/plugin.html#%E6%B3%A8%E5%86%8C%E6%8F%92%E4%BB%B6%E9%85%8D%E7%BD%AE-beta) 9 | - 优化: 插件的禁用/启用逻辑以及函数工具的禁用/启用逻辑 -------------------------------------------------------------------------------- /changelogs/v3.4.16.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | - [gewechat] [修复每次启动astrbot都需要扫码的问题](https://github.com/Soulter/AstrBot/commit/fd5d7dd37a6d74f81a148bbebef8516aa0cb5540) 4 | - [core] [Provider 重复时不直接报错闪退](https://github.com/Soulter/AstrBot/commit/b61f9be18db9a6b8b3c5b6b36553f66dd2b79375) https://github.com/Soulter/AstrBot/issues/265 5 | - [core] [弱化更新报错](https://github.com/Soulter/AstrBot/commit/0ba0150fd8ff2062dbe83889163888ba3e33bd49) https://github.com/Soulter/AstrBot/issues/267 6 | - 修复 webui 无法从本地上传插件的问题 -------------------------------------------------------------------------------- /changelogs/v3.4.17.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | - [beta] 支持群聊内基于概率的主动回复 4 | - openai tts 更换模型 #300 5 | - 增加模型响应后的插件钩子 6 | - 修复 相同type的provider共享了记忆 7 | - 优化 人格情景在发现格式不对时仍然加载而不是跳过 #282 8 | - 修复 Gemini函数调用时,parameters为空对象导致的错误 by @Camreishi 9 | - 修复 弹出记录报错的问题 #272 10 | - 优化 移除默认人格 11 | - 优化 未启用模型提供商时的异常处理 -------------------------------------------------------------------------------- /changelogs/v3.4.18.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | - fix: 修复主动概率回复关闭后仍然回复的问题 #317 4 | - fix: 尝试修复 gewechat 群聊收不到 at 的回复 #294 5 | - perf: 移除了默认人格 6 | - fix: 修复HTTP代理删除后不生效 #319 7 | - fix: 调用Gemini API输出多余空行问题 #318 8 | - feat: 添加硅基流动模版 9 | - fix: 硅基流动 not a vlm 和 tool calling not supported 报错 #305 #291 10 | - perf: 回复时艾特发送者之后添加空格或换行 #312 11 | - fix: docker容器内时区不对导致 reminder 时间错误 12 | - perf: siliconcloud 不支持 tool 的模型 -------------------------------------------------------------------------------- /changelogs/v3.4.19.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | 1. 支持接入企业微信(测试) 4 | 2. 修复速率限制不可用的问题 5 | 3. gewechat 回调接口默认暴露在所有 IP 6 | 4. 适配 Azure OpenAI 7 | 5. 修复请求 gemini 出现 KeyError 'candidates' 的错误 8 | 6. 将 /reset /persona 挪入管理员指令 #308 9 | 7. 支持通过 /alter_cmd 设置所有指令是否只能管理员操作 10 | 8. /plugin 指令支持查看插件注册的指令和指令组 11 | 9. 插件注册指令支持传入指令的描述以方便 /plugin 查看。需要写在函数的第一行的 docstring 中。 12 | 10. 修复 schema 中 object hint 不显示 #290 13 | 11. feat: 优化插件市场的访问速度 -------------------------------------------------------------------------------- /changelogs/v3.4.20.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | > 由于重写了会话记录部分,更新此版本后,将会造成之前的对话记录清空(但没有被删除)。 4 | > 关于更好的对话管理,如果有任何报错或者优化建议,请直接提交 issue~ 5 | 6 | 1. 更好的对话管理,支持 /ls, /del, /new, /switch, /rename 指令来操作对话。 7 | 2. 人格情境跟随对话。每个对话支持独立设置人格情境,只需要 /persona 指令切换即可。 8 | 3. 支持使用 LLM 辅助分段回复 #338 9 | 4. 优化 aiocqhttp 适配器对用户非法输入的处理 10 | 5. 优化插件页面 11 | 6. 修复权限过滤算子导致的问题 #350 12 | 7. 修复级联指令组时出现载入错误的问题 #366 13 | 8. 修复代码执行器的一个typo by @eltociear 14 | 9. 修复指令组情况下可能造成多指令出触发的问题 15 | 10. 添加屏蔽无权限指令回复的功能 #361 -------------------------------------------------------------------------------- /changelogs/v3.4.21.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | > 由于重写了会话记录部分,更新此版本后,将会造成之前的对话记录清空(但没有被删除)。 4 | > 关于更好的对话管理,如果有任何报错或者优化建议,请直接提交 issue~ 5 | 6 | 1. 修复 reminder 时区问题 7 | 2. 面板支持重载单个插件 #297 8 | 3. 面板支持列表展示插件市场 9 | 4. 文字转图片支持自定义字数阈值(配置->其他配置) 10 | 5. 面板更好的列表可视化 #274 11 | 6. 面板支持查看插件行为 12 | 7. 支持设置 timeout 超时时间参数,防止思考模型太长达到超时时间。(需要重新配置服务提供商或者在服务提供商 config 中配置 timeout 参数) #378 13 | 8. openrouter 报错 no endpoints found that support tool use #371 14 | 9. 修复插件 metadata 不生效的问题 15 | 10. 修复不支持图片的模型请求异常 16 | 11. 修复 reminder 无法删除的问题 17 | 12. 修复 /model 切换不了模型的问题 18 | 13. 插件支持设置优先级 19 | 14. 聊天增强图像转述支持自定义 provider id。#274 20 | -------------------------------------------------------------------------------- /changelogs/v3.4.22.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | 1. fix: 400 Bad Request: The browser (or proxy) sent a request that this server could not understand. #396 4 | 2. remove: 移除了 put_history_to_prompt。当主动回复时,将群聊记录将自动放入prompt,当未主动回复但是开启群聊增强时,群聊记录将放入system prompt 5 | 3. fix: 插件错误信息点击关闭没反应 #394 6 | 4. fix: 自部署文转图不生效 #352 7 | 5. fix: Google Search 报 429 错误时,放宽 Exception 至其他搜索引擎 #405 8 | 6. fix: 使用 Google Gemini (OpenAI 兼容)的部分情况下联网搜索等函数调用工具没被调用 #342 9 | 7. fix: 修复尝试弹出最早的记录失效的问题 10 | 8. fix: 移除了分段回复llm提示词辅助 11 | 9. perf: 当图片数据为空时不加入上下文 #379 12 | 10. 修复 dify 返回的结果带有多行数据时的 json 解析异常导致返回值为空的问题 #298 by @zhaolj -------------------------------------------------------------------------------- /changelogs/v3.4.23.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | 0. ✨ 新增: 支持 海豚 AI(FishAudio) TTS API #433 by @Cvandia 4 | 1. 🐛 修复: 当群聊主动回复时,不会带上人格的Prompt #419 5 | 2. ✨ 新增: 支持展示插件是否有更新 6 | 3. 👌 优化: 增加DIFY超时时间 #422 7 | 4. 🐛 修复: 自部署文转图不生效 #352 8 | 5. 🐛 修复: 修复 qq 回复别人的时候也会触发机器人, Onebot at 使用 string #330 9 | 6. 👌 优化: 增加DIFY超时时间 #422 10 | 7. 🐛 修复: 重启gewe的时候机器人会疯狂发消息 #421 11 | 8. 🐛 修复: 修复子指令设置permission之后会导致其一定会被执行 #427 -------------------------------------------------------------------------------- /changelogs/v3.4.24.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | 0. ✨ 新增: 支持正则表达式匹配触发机器人,机器人在某一段时间内持续唤醒(不用输唤醒词)。(安装 astrbot_plugin_wake_enhance 插件) 4 | 2. ✨ 新增: 可以通过 /tts 开关TTS,通过 /provider 更换 TTS #436 5 | 3. ✨ 新增: 管理面板支持设置 GitHub 反向代理地址以优化中国大陆地区下载 AstrBot 插件的速度。(在管理面板-设置页) 6 | 4. 🐛 修复: 修复指令不经过唤醒前缀也能生效的问题。在引用消息的时候无法使用前缀唤醒机器人 #444 7 | 5. 🐛 修复: 修复 Napcat 下戳一戳消息报错 8 | 6. 👌 优化: 从压缩包上传插件时,去除仓库 -branch 尾缀 9 | 7. 🐛 修复: gemini 报错时显示 apikey 10 | 8. 🐛 修复: drun 不支持函数调用的报错 11 | 9. 🐛 修复: raw_completion 没有正确传递导致部分插件无法正常运作 #439 -------------------------------------------------------------------------------- /changelogs/v3.4.25.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | 1. ✨ 新增: 支持接入飞书(Lark)。支持飞书文字、图片。 4 | 2. ✨ 新增: 添加月之暗面配置模板 #446 5 | 3. ✨ 新增: Gewechat 支持文件输出 6 | 4. 🐛 修复: 修复gewechat无法at人和发语音失败的问题 #447 #438 7 | 5. 🐛 修复: 修复qq在@和回复开启的情况下转发消息异常的问题 8 | 6. 🐛 修复: GitHub 加速镜像没有正确被应用 9 | 7. 🐛 优化: 平台将显示不受支持的消息段 -------------------------------------------------------------------------------- /changelogs/v3.4.26.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | 1. ✨ 新增: 支持 Webhook 方式接入 QQ 官方机器人接口 4 | 2. ✨ 新增: 支持完善的 Dify Chat 模式对话管理,包括 /new /switch /del /ls /reset 均已适配 Dify Chat 模式。 5 | 3. ✨ 新增: 支持基于对数函数的分段回复延时时间计算 #414 6 | 4. ✨ 新增: 支持设置管理面板的端口号 7 | 5. ✨ 新增: 支持对大模型的响应进行内容审查 #474 8 | 6. 🐛 修复: gewechat 不能发送主动消息 #402 9 | 7. 🐛 修复: dify Chat 模式无法重置会话 #469 10 | 8. 🐛 修复: ensure result is retrieved again to handle potential plugin chain replacements 11 | 9. 🐛 优化: 将 Gewechat 所有事件下发到流水线供插件开发 12 | 10. 🐛 修复: correct dashboard update tooltip typo by @Akuma-real 13 | -------------------------------------------------------------------------------- /changelogs/v3.4.27.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | 1. ✨ 新增: 支持日语版本的 Readme by @eltociear 4 | 2. ✨ 新增: 主动回复支持白名单 #488 5 | 3. ⚡ 优化: 面板数据展示图表的时区问题 #460 6 | 4. ⚡ 优化: 针对 id 对模型号进行排序以适配 OneAPI 乱序情况 #384 7 | 5. ✨ 新增: 支持对大模型的响应进行内容审查 #474 8 | 6. 🐛 修复: 修复保存插件配置时没有检查类型合法性的问题 9 | 7. 🐛 修复: 尝试修复 Gemini empty text 相关报错 10 | 8. 🐛 修复: dify 不能正常使用 set/unset 指令定义动态变量 #482 11 | 9. 🐛 修复: 不能在 Webhook 模式下的 QQ 官方 API 私聊 #484 12 | 10. 🐛 修复: 在没有触发并且没通过安全审查的情况下仍然发送了未通过消息 13 | 11. 🐛 修复: /del 指令导致的相关异常 14 | 12. 🐛 修复: 在 Gewechat 中不能先写内容后 @ 机器人 #492 -------------------------------------------------------------------------------- /changelogs/v3.4.28.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | 1. ✨ 新增: 管理面板支持搜索插件 4 | 2. ✨ 新增: 支持传递 OneBot 的 notice, request 事件类型,如戳一戳,进退群请求等 5 | 3. ✨ 新增: 插件支持自定义过滤算子 by @AraragiEro 6 | 4. ✨ 新增: 添加命令和命令组的别名支持 by @Cvandia 7 | 4. ✨ 新增: 提供了一个方法以删除分段回复后的某些字符,如末尾的标点符号。 by @Soulter and @Nothingness-Void 8 | 5. ⚡ 优化: 优化了分段回复和回复时at,引用都打开时的一些体验性问题 9 | 7. 🐛 修复: 分段回复导致了不完全的非 LLM 输出 #503 10 | 8. 🐛 修复: 添加 no_proxy 环境变量以支持本地请求, 修复在代理状态下时的 502 错误当通过 LMStudio, Ollama 本地部署 LLM 时 #504 #514 11 | 9. 💡🐛 修复: 修复转发消息的字数阈值功能 #510 12 | 10. 💡🐛 修复: 修复 Dify 下无法主动回复的问题 #494 -------------------------------------------------------------------------------- /changelogs/v3.4.29.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | 1. ✨ 新增: gemini source 初步支持对 API Key 进行负载均衡请求 #534 4 | 2. ✨ 新增: 开启对话隔离的群聊以及私聊下,非 op 可以可以使用 /del 和 /reset #519 5 | 3. ✨ 新增: 事件钩子支持 yield 方式发送消息 6 | 4. ⚡ 优化: 查询模型列表时,可以显示当前使用的模型名称 #523 7 | 5. ⚡ 优化: 更换为预编译指令的方式处理指令组指令 8 | 6. 🐛 修复: resolve KeyError when current conversation is not in paginated list 9 | 7. 🐛 修复: 修复指令组的情况下,Permission Filter 对子指令失效的问题 10 | 8. 🐛 修复: 🐛 fix: 修复 reminder rm失败 #529 11 | 9. 🐛 修复: 🐛 fix: reminder 时区问题 #529 12 | 10. 🐛 修复: 修复 Dify 下无法主动回复的问题 #494 13 | 11. 🐛 修复: 添加代码执行器 Docker 宿主机绝对路径配置及相关功能以修复 Docker 下无法使用代码执行器的问题 #525 14 | 12. 🐛 修复: gewechat 微信群聊情况下可能导致 unknown 的问题 #537 -------------------------------------------------------------------------------- /changelogs/v3.4.3.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | 1. 修复了 reminder 插件可能不会触发回调的问题。 4 | 2. 修复了 telegram 插件不可用的问题。 5 | 3. 修复了 qq_official 无法发图的问题。 6 | 4. 修复事件监听器会让 WakeStage 失效的问题。 7 | 5. 修复 websearch 在 cmd_config 中失效的问题。 8 | 3. 支持通过 Google GenAI 访问 Gemini 模型,而不需要使用 Gemini 对 OpenAI 的兼容 API。详见文档。 9 | 4. 支持对插件禁用/启用。/plugin off/on 10 | 5. 支持基于 Docker 的沙箱化代码执行器。(Beta 测试)详见文档。 11 | 6. 支持接入 Dify LLMOps 平台。详见文档。 12 | 7. 适配器类插件支持设置默认配置模板。 13 | 8. 优化了部分指令的持久化记忆。如 /tool 的禁用、/provider 的选择都将持久化保存,每次启动时不需要重新设置。 14 | 9. 优化了 glm-4v-flash 模型。其只支持一张图。 -------------------------------------------------------------------------------- /changelogs/v3.4.30.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | 1. ‼️🐛 修复: 修复某些情况下导致插件报错 AttributeError 的问题 #549 4 | 2. ✨ 新增: add xAI template 5 | 3. 🐛 修复: 修复 dify 无法使用事件钩子的问题以及出现 GeneratorExit 的问题 #533 #264 -------------------------------------------------------------------------------- /changelogs/v3.4.31.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | > 提示:改动范围较大 4 | 5 | 1. ✨ 新增: 添加对 Anthropic Claude 的支持 by @Rt39 6 | 2. ✨ 新增: 支持阿里云百炼应用(dashscope)智能体、工作流 #552 by @Soulter 7 | 3. ✨ 新增: 支持 AstrBot 更新使用 Github 加速地址 by @Fridemn 8 | 4. ✨ 新增: 适配多节点的转发消息,添加新的消息段 `Nodes` 9 | 5. ✨ 新增: 支持在管理面板重启(设置页) 10 | 6. ✨ 新增: 前端支持以列表展示正式版和开发版的列表 11 | 7. ✨ 新增: 支持插件禁止默认的llm调用(event.should_call_llm())#579 12 | 8. 🍺 重构: 支持更大范围的热重载以及管理面板将平台和提供商配置独立化 by @Soulter 13 | 9. ⚡ 优化: 启动时检查端口占用 by @Fridemn 14 | 10. ⚡ 优化: 添加控制台关闭自动滚动按钮 by @Fridemn 15 | 11. ⚡ 优化: 在聊天页面添加粘贴图片的快捷键提示 #557 16 | 12. 🐛 修复: 修复 webchat 未处理 base64 的问题 by @Raven95676 17 | 13. 🐛 修复: 修复 aiocqhttp_platform_adapter 文件相关判断逻辑 by @Raven95676 18 | 14. ‼️🐛 修复: 修复 gemini 请求时出现多次不支持函数工具调用最后 429 的问题 -------------------------------------------------------------------------------- /changelogs/v3.4.32.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | 4 | 1. ✨ 新增: Add a draggable iframe for tutorial links and enhance platform configuration UI 5 | 2. ✨ 新增: 集成 astrbot_plugin_telegram/企业微信 至 astrbot 6 | 3. ✨ 新增: openai_source 支持传入任何自定义参数以适配 Ollama 和 FastGPT 等 provider 7 | 4. ✨ 新增: Telegram 适配器中支持 @ 唤醒 8 | 5. ✨ 新增: 添加面板下载按钮置灰 by @Fridemn 9 | 6. ✨ 新增: 添加 SenseVoice 语音转文本(STT)服务 by @diudiu62 10 | 7. ⚡ 优化: Increase forward threshold from 200 to 1500 in default configuration 11 | 8. ⚡ 优化: 添加控制台关闭自动滚动按钮 by @Fridemn 12 | 9. 🐛 修复: 修复前端面板部分页面刷新后的 404 错误 13 | 10. 🐛 修复: 修复某些情况下热重载 服务提供商 时可能没有正确应用的问题 14 | 11. 🐛 修复: 修复 Telegram 适配器中未处理 base64 的问题 @Raven95676 15 | 12. 🐛 修复: 修复 Dify 主动回复报错的问题 #616 -------------------------------------------------------------------------------- /changelogs/v3.4.33.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | 1. ✨ 新增: add English README by @CAICAIIs 4 | 2. ✨ 新增: perf: 优化网页录音 [#283](https://github.com/Soulter/AstrBot/issues/283) by @Fridemn 5 | 3. ✨ 新增: 添加对于 Edge-TTS 的支持 [#471](https://github.com/Soulter/AstrBot/issues/471) by @Fridemn 6 | 4. ⚡ 优化: 为防止输入一大堆 k,改 k 键为 Ctrl 键;改为长按录音,松手结束;为防止误触改为只有点击输入框之后才会生效 by @Fridemn 7 | 5. ⚡ 优化: 插件市场非列表视图能够正常搜索 [#640](https://github.com/Soulter/AstrBot/issues/640) by @Fridemn 8 | 6. ⚡ 优化: 插件市场帮助按钮 tooltip 移入时会消失无法点击其中链接,更改为按钮触发 by @Quirrel-zh 9 | 7. ‼️‼️ 🐛 修复: v3.4.32 无法记忆历史的会话 [#630](https://github.com/Soulter/AstrBot/issues/630) 10 | 8. ‼️🐛 修复: 钩子函数无法终止事件传播的问题;修复某些情况下终止事件传播后仍然会请求 LLM 的问题 11 | 9. ‼️🐛 修复: OneBot V11 通知类事件某些情况无法回复问题 by @CAICAIIs 12 | 10. 🐛 修复: Correct STT model path and improve logging in provider manager and pip installer 13 | 11. 🐛 修复: 由于已安装插件与插件市场中 name 不一致或 repo 链接大小写不一致导致的检测不到是否安装或有更新 by @Quirrel-zh -------------------------------------------------------------------------------- /changelogs/v3.4.35.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | 1. ✨ 新增: 添加 GPT-SoVits-Inference(GSVI) TTS 支持 #545 #351 by @Fridemn 4 | 2. ✨ 新增: Telegram 支持发送文件和语音 5 | 3. ✨ 新增: 完善插件在禁用/重载时的逻辑,添加 terminate() Star 父类方法 6 | 4. ✨ 新增: 添加 AstrBot 启动完成时的事件钩子;添加获取指定平台适配器(Platform)的接口 7 | 5. ✨ 新增: 分离本地插件和插件市场 8 | 6. ⚡ 优化: 代码执行器使用指令 `/pi file` 来指定上传文件以更好适配全平台; 9 | 7. ⚡ 优化: 切换 Provider 时如果没有打开 Provider 开关,自动打开。 10 | 8. ⚡ 优化: 为 switch_conv 的 index 参数添加类型判断 by @Kx-Y 11 | 9. ⚡ 优化: WebUI 缓存插件市场数据防止重复请求 12 | 10. ⚡ 优化: 插件市场搜索同时支持对插件描述进行搜索 13 | 11. ⚡ 优化: 将 Flask 初始化时允许的最大文件体积设置为 128 MB by @inori-3333 14 | 12. ⚡ 优化: 插件市场、更新项目的视觉反馈 15 | 13. ‼️‼️ 🐛 修复: 插件 AsyncGenerator 在没有执行 yield 语句的情况下设置事件结果无法被处理的问题 16 | 14. ‼️‼️ 🐛 修复: telegram @ 任何人都会触发机器人 #669 17 | 15. ‼‼️ 🐛 修复: wecom 加载失败的问题 #659 18 | 16. ‼‼️ 🐛 修复: gewechat 'TypeName' 解析错误 #680 #682 19 | 17. 🔧 Dev: 使用 ruff 格式化工程,添加了 pre-commit-ci -------------------------------------------------------------------------------- /changelogs/v3.4.36.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | 1. ✨ 新增: 支持插件会话控制 API 4 | 2. ✨ 新增: add template of LMStudio #691 5 | 3. ✨ 新增: 更好的插件卡片的 UI,插件卡片支持显示 logo,推荐插件页面 6 | 4. ✨ 新增: 支持当消息只有 @bot 时,下一条发送人的消息**直接唤醒机器人** #714 7 | 5. ⚡ 优化: Webchat 和 Gewechat 的图片、语音等主动消息发送 #710 8 | 6. ⚡ 优化: 完善了插件的启用和禁用的生命周期管理 9 | 7. ⚡ 优化: 安装插件/更新插件/保存插件配置后直接热重载而不重启;优化了 plugin 指令 10 | 8. 🐛 修复: 主动人格情况下人格失效的问题 #719 #712 11 | 9. 🐛 修复: 404 error after installing plugins 12 | 10. 🐛 修复: telegram cannot handle /start #620 13 | 11. 🐛 修复: 修复插件在带了 __del__ 之后无法被禁用和重载的问题 14 | 12. 🐛 修复: context.get_platform() error 15 | 13. 🐛 修复: Telegram 适配器使用代理地址无法获取图片 #723 -------------------------------------------------------------------------------- /changelogs/v3.4.37.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | 1. ✨ 新增: 支持接入钉钉 #643 4 | 2. ✨ 新增: 支持设置私聊是否需要唤醒前缀唤醒 [#735](https://github.com/Soulter/AstrBot/issues/735) 5 | 3. 🐛 修复: 无法正常保存插件的 list 类型配置 #737 6 | 4. 🐛 修复: 部分情况下使用 aiocqhttp 报错 int 不能与 str 进行 '+' 操作的问题 -------------------------------------------------------------------------------- /changelogs/v3.4.38.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | > Special thanks for all contributors and plugin developers and users who love AstrBot. 💖 4 | 5 | ## ✨ 新增的功能 6 | 7 | 1. 支持解析回复消息,支持 LLM 对所引用消息具有感知 #783 8 | 2. 支持 Dify 的文件、图片、视频、音频输出 #819 9 | 3. QQ 下支持嵌套转发(napcat) @zouyonghe 10 | 4. 配置页样式重写,更紧凑的 WebUI 配置 11 | 12 | ## 🎈 功能性优化 13 | 14 | 1. 使用系统时间而不是 UTC+8 时间作为默认时间以适应海外用户需求 @roeseth 15 | 2. 在对话隔离情况下也可以将整个群聊加入白名单 #746 16 | 3. 在调用插件异常时更完整的报错输出 17 | 4. gewechat 下对已知且没有业务处理的事件类型不显示详细日志 @diudiu62 18 | 5. 优化 WebUI 悬浮文档 @IGCrystal 19 | 6. 支持自定义 WebUI、Wecom Webhook Server, QQ Official Webhook Server 的 host #821 20 | 7. Dify 下当只有图片输入时的默认 prompt 防止一些报错 #837 21 | 22 | ## 🐛 修复的 Bug 23 | 24 | 1. fishaudio 默认 baseurl 不可用 25 | 2. gewechat 下重复登录后提示设备不存在导致无法重新登陆 @beat4ocean 26 | 3. gewechat 下用户本人发消息会触发消息回复 @beat4ocean 27 | 4. 钉钉 WebUI 文档不显示 28 | 5. 更新插件后插件热重载不完全、函数工具重复添加 29 | 6. OpenAI TTS API TypeError 报错 #755 30 | 7. EdgeTTS 部分情况下无法使用 @Soulter @需要哦 31 | 8. QQ 官方机器人平台下发送 base64 图片消息段报错 @Soulter @shuiping233 32 | 9. QQ 官方机器人平台下命令参数报错信息无法正常发送 @shuiping233 33 | 10. WebUI 错误地显示未知更新 34 | 11. 部分情况下文件无法上传到 Telegram 群组 #601 35 | 12. 插件管理的插件简介太长导致 “帮助”“操作”图标不显示 #790 36 | 13. LLOnebot 合并消息转发错误 #842 37 | 14. model_config 中自定义的配置项(如温度)类型自动变回 string #854 38 | 39 | ## 🧩 新增的插件 40 | 41 | 1. astrbot_plugin_image_understanding_Janus-Pro - 使用deepseek-ai/Janus-Pro系列模型为本地模型提供的图片理解补充 @xiewoc 42 | 2. astrbot_plugin_moyurenpro - 摸鱼人日历,支持自定义时间时区,自定义api,支持立即发送,工作日定时发送。 @quirrel-zh @DuBwTf 43 | 3. astrbot_plugin_wechat_manager - 微信关键字好友自动审核、关键字邀请进群。@diudiu62 44 | 4. astrbot_plugin_qwq_filter - qwq 思考过滤工具 @beat4ocean 45 | 5. astrbot_plugin_chatsummary - 一个通过拉取历史聊天记录,调用LLM大模型接口实现消息总结功能。@laopanmemz 46 | 6. astrBot_PGR_Dialogue - 检测到部分战双角色的名称(或别称)时,有概率发送一条语音文本 @KurisuRee7 47 | 7. astrbot_plugin_bv - 解析群内https://www.bilibili.com/video/BV号/ 的链接并获取视频数据与视频文件,以合并转发方式发送 @haliludaxuanfeng 48 | 8. astrbot_plugin_gemini_exp - 让你在AstrBot调用Gemini2.0-flash-exp来生成图片或者p图。Gemini2.0-flash-exp为原生多模态模型,其既是语言模型,也是生图模型,因此能够对图像使用简单的自然语言命令进行处理。@Elen123bot 49 | 9. astrbot_plugin_sjzb - 随机生成绝地潜兵2游戏中一组4个战备配置 @tenno1174 50 | 10. astrbot_plugin_picture_manager - 图片管理插件,允许用户通过自定义触发指令从API或直接URL获取图片。@bigshabei 51 | 11. astrbot_plugin_bilibiliParse - 解析哔哩哔哩视频,并以图片的形式发送给用户 @7Hello12 52 | 12. astrbot_plugin_sensoji - 这是一个模拟日本浅草寺抽签功能的插件。用户可以通过发送 /抽签 命令随机抽取一个签文,获取运势提示。签文包含吉凶结果(如“大吉”、“凶”等)以及对应的运势描述。 @Shouugou 53 | 13. astrbot_plugin_videosummary - 使用 bibigpt 实现视频总结 @kterna 54 | 14. astrbot_plugin_InitiativeDialogue - 使 bot 在用户长时间未发送消息时主动与用户对话的插件 @advent259141 55 | 15. astrbot_plugin_emoji - 基于达莉娅综合群娱插件的表情包制作插件,仅保留了@其他群员制作表情包的部分。由桑帛云API提供表情包制作。@KurisuRee7 56 | 16. astrbot_plugin_videos_analysis - 聚合视频分享链接解析(仅测试过napcat) @miaoxutao123 57 | 17. astrbot_plugin_daily_news - 每日 60 秒新闻推送插件 - 自动推送每日热点新闻 @anka-afk -------------------------------------------------------------------------------- /changelogs/v3.4.39.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | 1. 默认账户密码登录成功后弹出修改警告 4 | 2. 将 WebUI 默认 host 改变回 v3.4.38 之前的版本以减少兼容性问题。 -------------------------------------------------------------------------------- /changelogs/v3.4.4.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | 1. 支持通过 /set 设置持久化的会话变量, 方便 Dify App 输入变量 4 | 2. 管理面板支持 Web Chat 5 | 3. 管理面板支持手动安装 Pip 库, 在 `控制台` 页中可找到 6 | 7 | -------------------------------------------------------------------------------- /changelogs/v3.4.5.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | - 支持接入 STT(语音转文字)Provider 4 | - 内置支持 OpenAI Whisper API/本地运行模型。[看这里](https://astrbot.lwl.lol/use/whisper.html) 5 | - WebChat 支持语音输入 6 | - WebChat 支持显示当前 Provider 状态 7 | - 优化了 WebChat 在没有消息返回时的处理方式 8 | - 修复了 reminder 在初始化历史待办时没有正常传入 session_id 的问题 9 | - 代码执行器在成功回复后清空文件 buffer。 -------------------------------------------------------------------------------- /changelogs/v3.4.6.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | - 文件和语音功能适配 Lagrange 4 | - 面板文件更新检查和引导提示 5 | - WebUI AboutPage 关于页 6 | - 支持并完善服务提供商(Provider)默认配置模板接口 7 | - 修复 WebUI 配置页官方文档链接 404 的问题 8 | - 修复 WebUI WebChat 刷新时 404 的问题 9 | - 优化 download_file 的 SSL 连接错误处理 -------------------------------------------------------------------------------- /changelogs/v3.4.7.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | - 更好的人格情景管理 4 | - 移除了不常用的人格提示词集 5 | - 优化webchat长连接的处理逻辑 6 | - 修复 tool 为空时部分模型请求错误的问题 #239 -------------------------------------------------------------------------------- /changelogs/v3.4.8.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | - 支持 Gewechat 接入微信个人号(文字交互) 4 | - 支持回复时 At 和引用发送者 #241 5 | - 清除残留的 personalities -------------------------------------------------------------------------------- /changelogs/v3.4.9.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | - AstrBot 新域名:astrbot.app 4 | - LLM额外唤醒词与机器人唤醒词冲突时的处理 5 | - 调整部分日志的严重级别 6 | - 下载管理面板时显示提示、下载进度和下载速度 -------------------------------------------------------------------------------- /changelogs/v3.5.1.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | > 📢 在升级前,请完整阅读本次更新日志。 4 | 5 | ## ✨ 新增的功能 6 | 7 | 1. 适配 `gemini-2.0-flash-exp-image-generation` 对图片模态的输入 [#1017](https://github.com/Soulter/AstrBot/issues/1017) 8 | 2. 在 MessageChain 类中添加 at 和 at_all 方法,用于快速添加 At 消息 @left666 9 | 3. Gewechat Client 增加获取通讯录列表接口 10 | 4. 支持 /llm 指令快捷启停 LLM 功能 [#296](https://github.com/Soulter/AstrBot/issues/296) 11 | 12 | ## 🎈 功能性优化 13 | 14 | 1. Edge TTS 支持使用代理 15 | 2. 在 Lifecycle 新增插件资源清理逻辑 @Raven95676 16 | 3. Docker 镜像提供内置 FFmpeg [#979](https://github.com/Soulter/AstrBot/issues/979) 17 | 4. 优化无对话情况下设置人格的反馈 @Raven95676 18 | 5. 若禁用提供商,自动切换到另一个可用的提供商 @Raven95676 19 | 6. openai_source 同步支持随机请求均衡,同时优化 LLM 请求逻辑的异常处理 20 | 7. 保存 shared_preferences 时强制刷新文件缓冲区 21 | 8. 优化空 At 回复 @advent259141 22 | 23 | ## 🐛 修复的 Bug 24 | 25 | 1. 插件更新时没有正确应用加速地址 26 | 2. newgroup 指令名显示错误 27 | 28 | ## 🧩 新增的插件 29 | 30 | 待补充 31 | -------------------------------------------------------------------------------- /changelogs/v3.5.10.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | 1. 新增: 支持接入个人微信(WeChatPadPro)替换 gewechat 方式。 4 | 2. 新增:接入 PPIO 派欧云 5 | 3. 新增:支持接入 Minimax TTS 6 | 3. ‼️修复:Docker 下重启 AstrBot 会导致 astrbot 容器进程退出的问题。 7 | 4. 优化:速率限制功能 8 | 5. 优化:QQ 和 Telegram 下,群聊的 @ 信息也将发送给模型以获得更好的回复、QQ 支持 @ 全体成员的解析。 9 | 6. 优化:WebUI 配置项支持代码编辑器模式! 10 | 7. 优化:语音组件将单独发送以保证全平台兼容性 11 | 8. 优化:QQ 下,屏蔽 QQ 管家(qq=2854196310) 的所有消息。 -------------------------------------------------------------------------------- /changelogs/v3.5.11.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | 1. 新增:火山引擎 TTS 4 | 2. 修复:修复了 WeChatPadPro 在重新登录时为新设备的问题 5 | 2. ‼️修复:微信公众号(个人认证或者未认证)的情况下能接收但无法回复消息的问题 6 | 3. 修复:Minimax TTS 相关问题 7 | 4. 优化:登录界面侧边栏、关于页面样式,修复如果此前已经登录但未自行跳转的问题 -------------------------------------------------------------------------------- /changelogs/v3.5.12.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | 1. 新增:支持 MCP 的 Streamable HTTP 传输方式。详见 [#1637](https://github.com/Soulter/AstrBot/issues/1637) 4 | 2. 新增:支持 MCP 的 SSE 传输方式的自定义请求头。详见 [#1659](https://github.com/Soulter/AstrBot/issues/1659) 5 | 3. 优化:将 /llm 和 /model 和 /provider 指令设置为管理员指令 6 | 4. 修复:修复插件的 priority 部分失效的问题 7 | 5. 修复:修复 QQ 下合并转发消息内无法发送文件等问题,尽可能修复了各种文件、语音、视频、图片无法发送的问题 8 | 6. 优化:Telegram 支持长消息分段发送,优化消息编辑的逻辑 9 | 7. 优化:WebUI 强制默认修改密码 10 | 8. 优化:移除了 vpet 11 | 9. 新增:插件接口:支持动态路由注册 12 | 10. 优化:CLI 模式下的插件下载 13 | 11. 新增:WeChatPadPro 对接获取联系人接口 14 | 12. 新增:T2I、语音、视频支持文件服务 15 | 13. 优化:硅基流动下某些工具调用返回的 argument 格式适配 16 | 14. 优化:在使用 /llm 指令关闭后重启 AstrBot 后,模型提供商未被加载 17 | 15. 新增:新增基于 FAISS + SQLite 的向量存储接口 18 | 16. 新增:Alkaid Page 19 | -------------------------------------------------------------------------------- /changelogs/v3.5.13.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | 1. 新增:WebUI 支持暗夜模式。 4 | 2. 修复:修复 WebUI Chat 接口的未授权访问安全漏洞、插件 README 可能存在的 XSS 注入漏洞。 5 | 3. 优化:优化 Vec DB 在 indexing 过程时的数据库事务处理。 6 | 4. 修复:WebUI 下,插件市场的推荐卡片无法点击帮助文档的问题。 7 | 5. 新增:知识库。 8 | 6. 新增:WebUI 提供商测试功能,一键检测可用性。 9 | 7. 新增:WebUI 提供商分类功能,按能力分类提供商。 10 | -------------------------------------------------------------------------------- /changelogs/v3.5.2.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | > 📢 在升级前,请完整阅读本次更新日志。 4 | 5 | ## ✨ 新增的功能 6 | 7 | 1. 安装完插件后自动弹出插件仓库 README 对话框 @zhx8702 8 | 4. 支持阿里云百炼 TTS@Soulter 9 | 5. 支持 Telegram MarkdownV2 渲染 @Soulter 10 | 6. 支持 钉钉 Markdown 渲染 @Soulter 11 | 6. 增加对 Gemini 系列模型的输入安全设置参数支持 @AliveGh0st 12 | 7. 支持手动设置时区以应对容器、国外用户的时区问题 @anka-afk @Raven95676 @Soulter 13 | 8. 插件市场显示帮助按钮 @Soulter 14 | 15 | ## 🎈 功能性优化 16 | 17 | 1. WebUI 的日志通信使用 SSE 替代 Websockets @Soulter 18 | 2. 在发送消息之前统一检查消息内容是否为空, 不允许发送空消息, 以解决该消息内容不支持查看以及 Gemini 返回 `` 问题 @anka-afk 19 | 3. 更新 Dify 平台链接为官方域名 by @Captain-Slacker-OwO 20 | 4. 人格 prompt 输入框支持调节高度 @Soulter 21 | 22 | ## 🐛 修复的 Bug 23 | 24 | 1. 将最多携带对话数量修改回 `-1` 时出现报错 #1074 @anka-afk 25 | 2. 修复无法识别到函数调用异常的问题 by @Soulter 26 | 3. 修复 aiocqhttp 适配器下空白 plain 导致的 `the object is not a proper segment chain` 报错问题 @Soulter 27 | 4. 修复阿里百炼应用无法多轮会话的问题 @Soulter 28 | 29 | ## 🧩 新增的插件 30 | 31 | 待补充 32 | -------------------------------------------------------------------------------- /changelogs/v3.5.3.1.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | > 📢 在升级前,请完整阅读本次更新日志。 4 | > 此版本为针对 `v3.5.3` 的紧急修复版本 5 | 6 | ## ✨ 新增的功能 7 | 8 | 1. Telegram、Webchat、QQ官方机器人平台(私聊)支持流式输出(实验性)。@Soulter @Raven95676 @anka-afk 9 | 2. 支持针对不同消息平台开启/关闭插件 @zhx8702 @Raven95676 @Soulter 10 | 3. 插件市场支持显示 Star 个数、插件管理支持插件帮助对话框 @kterna 11 | 4. 飞书平台支持主动消息发送 @Soulter 12 | 5. Telegram 平台适配显示指令列表,支持自动补全 @Raven95676 13 | 6. 新增配置项允许配置当超出最多携带对话数量时,一次性丢弃多少条旧消息 @Rail1bc 14 | 7. StarTool 新增获取插件数据目录接口 @Raven95676 15 | 16 | ## 🎈 功能性优化 17 | 18 | 1. 优化 /his 指令对函数调用的显示 @anka-afk 19 | 2. QQ 官方机器人支持对同一条消息多次回复 @kuangfeng 20 | 21 | ## 🐛 修复的 Bug 22 | 23 | 1. ‼️ 修复使用 gemini 时,函数数工具调用会重复调用已经在过去会话中调用过的工具 @Soulter 24 | 2. 修复使用 Gemini 模型时出现 的问题 @anka-afk 25 | 4. 修复使用 OneAPI + Gemini(openai) 传递空参数函数工具时可能报错的问题 @Soulter 26 | 5. 修复 permission 过滤算子的 raise_error 参数失效的问题 @Soulter 27 | 6. 修复函数调用时可能出现 `messages with role 'tool' must be a response to a preceeding message with 'tool_calls'` 报错的问题 @anka-afk 28 | 7. 修复 dify 下删除对话的报错问题 @Soulter 29 | 8. 修复人格预设对话多次插入上下文的问题 @Rail1bc 30 | 9. 修复了 event.get_sender_id() 返回值与函数注释不一致的问题 @zsbai 31 | 32 | 33 | ## 🧩 新增的插件 34 | 35 | 待补充 36 | -------------------------------------------------------------------------------- /changelogs/v3.5.3.2.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | > 📢 在升级前,请完整阅读本次更新日志。 4 | > 此版本为针对 `v3.5.3` 的紧急修复版本 5 | > 修复以下 BUG: 6 | > 1. 智谱 GLM 在函数工具有空参数时报错的问题。 7 | 8 | --- 9 | 10 | v3.5.3 11 | 12 | ## ✨ 新增的功能 13 | 14 | 1. Telegram、Webchat、QQ官方机器人平台(私聊)支持流式输出(实验性)。@Soulter @Raven95676 @anka-afk 15 | 2. 支持针对不同消息平台开启/关闭插件 @zhx8702 @Raven95676 @Soulter 16 | 3. 插件市场支持显示 Star 个数、插件管理支持插件帮助对话框 @kterna 17 | 4. 飞书平台支持主动消息发送 @Soulter 18 | 5. Telegram 平台适配显示指令列表,支持自动补全 @Raven95676 19 | 6. 新增配置项允许配置当超出最多携带对话数量时,一次性丢弃多少条旧消息 @Rail1bc 20 | 7. StarTool 新增获取插件数据目录接口 @Raven95676 21 | 22 | ## 🎈 功能性优化 23 | 24 | 1. 优化 /his 指令对函数调用的显示 @anka-afk 25 | 2. QQ 官方机器人支持对同一条消息多次回复 @kuangfeng 26 | 27 | ## 🐛 修复的 Bug 28 | 29 | 1. ‼️ 修复使用 gemini 时,函数数工具调用会重复调用已经在过去会话中调用过的工具 @Soulter 30 | 2. 修复使用 Gemini 模型时出现 的问题 @anka-afk 31 | 4. 修复使用 OneAPI + Gemini(openai) 传递空参数函数工具时可能报错的问题 @Soulter 32 | 5. 修复 permission 过滤算子的 raise_error 参数失效的问题 @Soulter 33 | 6. 修复函数调用时可能出现 `messages with role 'tool' must be a response to a preceeding message with 'tool_calls'` 报错的问题 @anka-afk 34 | 7. 修复 dify 下删除对话的报错问题 @Soulter 35 | 8. 修复人格预设对话多次插入上下文的问题 @Rail1bc 36 | 9. 修复了 event.get_sender_id() 返回值与函数注释不一致的问题 @zsbai 37 | 38 | 39 | ## 🧩 新增的插件 40 | 41 | 待补充 42 | -------------------------------------------------------------------------------- /changelogs/v3.5.3.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | > 📢 在升级前,请完整阅读本次更新日志。 4 | 5 | ## ✨ 新增的功能 6 | 7 | 1. Telegram、Webchat、QQ官方机器人平台(私聊)支持流式输出(实验性)。@Soulter @Raven95676 @anka-afk 8 | 2. 支持针对不同消息平台开启/关闭插件 @zhx8702 @Raven95676 @Soulter 9 | 3. 插件市场支持显示 Star 个数、插件管理支持插件帮助对话框 @kterna 10 | 4. 飞书平台支持主动消息发送 @Soulter 11 | 5. Telegram 平台适配显示指令列表,支持自动补全 @Raven95676 12 | 6. 新增配置项允许配置当超出最多携带对话数量时,一次性丢弃多少条旧消息 @Rail1bc 13 | 7. StarTool 新增获取插件数据目录接口 @Raven95676 14 | 15 | ## 🎈 功能性优化 16 | 17 | 1. 优化 /his 指令对函数调用的显示 @anka-afk 18 | 2. QQ 官方机器人支持对同一条消息多次回复 @kuangfeng 19 | 20 | ## 🐛 修复的 Bug 21 | 22 | 1. ‼️ 修复使用 gemini 时,函数数工具调用会重复调用已经在过去会话中调用过的工具 @Soulter 23 | 2. 修复使用 Gemini 模型时出现 的问题 @anka-afk 24 | 4. 修复使用 OneAPI + Gemini(openai) 传递空参数函数工具时可能报错的问题 @Soulter 25 | 5. 修复 permission 过滤算子的 raise_error 参数失效的问题 @Soulter 26 | 6. 修复函数调用时可能出现 `messages with role 'tool' must be a response to a preceeding message with 'tool_calls'` 报错的问题 @anka-afk 27 | 7. 修复 dify 下删除对话的报错问题 @Soulter 28 | 8. 修复人格预设对话多次插入上下文的问题 @Rail1bc 29 | 9. 修复了 event.get_sender_id() 返回值与函数注释不一致的问题 @zsbai 30 | 31 | 32 | ## 🧩 新增的插件 33 | 34 | 待补充 35 | -------------------------------------------------------------------------------- /changelogs/v3.5.5.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | ## 🐛 修复的 Bug 4 | 5 | 1. 修复 Gemini 下可能无法正常使用 Tools 的问题 @Raven95676 6 | 2. 修复 WebUI MCP 页面的一些问题 @Soulter 7 | -------------------------------------------------------------------------------- /changelogs/v3.5.6.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | > 🙁 Gewechat 已经停止维护,我们将更换更稳定的个人微信接入方式。如有问题请提交 issue。 4 | > 🧐 预告:接下来三个版本之内将会逐步上线 Live2D 桌宠、长期记忆(实验性)的功能。 5 | 6 | 1. Gewechat 相关 bug 修复(即使已经不可用 :( ) @BigFace123 @XiGuang @Soulter 7 | 2. 支持 CLI 命令行 @LIghtJUNction 8 | 3. 修复 QQ 下带有网址的指令可能无法识别的问题 @kkjzio 9 | 4. `reset` 指令优化 @anka-afk 10 | 5. Gemini 请求优化,支持 Gemini 思考信息设置 @Raven95676 11 | 6. 支持处理 MCP 服务器返回的图片等多模态信息 @Raven95676 12 | 7. 插件市场支持基于 Star 和 更新时间排序 @Soulter 13 | 8. 优化 QQ 下自动下载文件导致磁盘被占满的问题 @Soulter @anka-afk -------------------------------------------------------------------------------- /changelogs/v3.5.7.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | > Gewechat 已经停止维护,此版本提供了 `微信客服` 的接入方式,可以在直接微信内聊天。这是微信官方推出的接入方式,因此没有风控风险。详见 [AstrBot 接入企业微信](https://astrbot.app/deploy/platform/wecom.html)。此接入方式处于测试阶段,有问题请及时在 GitHub 上提交 Issue。 4 | 5 | 1. 支持接入微信客服。 -------------------------------------------------------------------------------- /changelogs/v3.5.8.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | 1. 支持接入微信公众平台,详见 [AstrBot - 微信公众平台](https://astrbot.app/deploy/platform/weixin-official-account.html) @Soulter 4 | 2. 优化 gemini_source 方法默认参数 @Raven95676 5 | 3. 优化 persona 错误显示 @Soulter -------------------------------------------------------------------------------- /changelogs/v3.5.9.md: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | 1. 重构: 采用更好的方式将文件上传到 NapCat 协议端,无需映射路径。**(需要前往 配置->其他配置 中配置`对外可达的回调接口地址`)** @Soulter @anka-afk 4 | 2. 修复: 单独发送文件时被认为是空消息导致文件无法发送的问题 @Soulter 5 | 3. 修复: Lagrange 下合并转发消息失败的问题 @Soulter 6 | 4. 修复: CLI 模式下路径问题导致 WebUI 和 MCP Server 无法加载的问题 @Soulter 7 | 5. 修复: 设置 Gemini 的 thinking_budget 前,先检查是否存在 @Raven95676 8 | 6. 修复: 修复企业微信和微信公众平台下无法应用 api_base_url 的问题 @Soulter 9 | 7. 优化: 分离 plugin 指令为指令组,优化 plugin 指令权限控制 @Soulter 10 | 8. 优化: WebUI 更直观的模型提供商选择 @Soulter 11 | 9. 优化: AstrBot 的重启逻辑 @Anchor 12 | 10. 新增: CLI 支持部分配置文件项的设定、支持插件管理和检测到插件文件变化时自动热重载 @Raven95676 13 | 11. 新增: 现已支持 Azure TTS @NanoRocky -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | # 当接入 QQ NapCat 时,请使用这个 compose 文件一键部署: https://github.com/NapNeko/NapCat-Docker/blob/main/compose/astrbot.yml 4 | 5 | services: 6 | astrbot: 7 | image: soulter/astrbot:latest 8 | container_name: astrbot 9 | restart: always 10 | ports: # mappings description: https://github.com/Soulter/AstrBot/issues/497 11 | - "6185:6185" # 必选,AstrBot WebUI 端口 12 | - "6195:6195" # 可选, 企业微信 Webhook 端口 13 | - "6199:6199" # 可选, QQ 个人号 WebSocket 端口 14 | - "6196:6196" # 可选, QQ 官方接口 Webhook 端口 15 | - "11451:11451" # 可选, 微信个人号 Webhook 端口 16 | environment: 17 | - TZ=Asia/Shanghai 18 | volumes: 19 | - ./data:/AstrBot/data 20 | # - /etc/timezone:/etc/timezone:ro 21 | - /etc/localtime:/etc/localtime:ro 22 | -------------------------------------------------------------------------------- /dashboard/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store -------------------------------------------------------------------------------- /dashboard/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 CodedThemes 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 | -------------------------------------------------------------------------------- /dashboard/README.md: -------------------------------------------------------------------------------- 1 | # AstrBot 管理面板 2 | 3 | 基于 CodedThemes/Berry 模板开发。 -------------------------------------------------------------------------------- /dashboard/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /dashboard/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | AstrBot - 仪表盘 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /dashboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astrbot-dashboard", 3 | "version": "1.0.0", 4 | "private": true, 5 | "author": "CodedThemes", 6 | "scripts": { 7 | "dev": "vite --host", 8 | "build": "vue-tsc --noEmit && vite build", 9 | "build-stage": "vue-tsc --noEmit && vite build --base=/vue/free/stage/", 10 | "build-prod": "vue-tsc --noEmit && vite build --base=/vue/free/", 11 | "preview": "vite preview --port 5050", 12 | "typecheck": "vue-tsc --noEmit", 13 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" 14 | }, 15 | "dependencies": { 16 | "@guolao/vue-monaco-editor": "^1.5.4", 17 | "@tiptap/starter-kit": "2.1.7", 18 | "@tiptap/vue-3": "2.1.7", 19 | "apexcharts": "3.42.0", 20 | "axios": "^1.6.2", 21 | "axios-mock-adapter": "^1.22.0", 22 | "chance": "1.1.11", 23 | "d3": "^7.9.0", 24 | "date-fns": "2.30.0", 25 | "highlight.js": "^11.11.1", 26 | "js-md5": "^0.8.3", 27 | "lodash": "4.17.21", 28 | "marked": "^15.0.7", 29 | "pinia": "2.1.6", 30 | "remixicon": "3.5.0", 31 | "vee-validate": "4.11.3", 32 | "vite-plugin-vuetify": "1.0.2", 33 | "vue": "3.3.4", 34 | "vue-router": "4.2.4", 35 | "vue3-apexcharts": "1.4.4", 36 | "vue3-print-nb": "0.1.4", 37 | "vuetify": "3.7.11", 38 | "yup": "1.2.0" 39 | }, 40 | "devDependencies": { 41 | "@mdi/font": "7.2.96", 42 | "@rushstack/eslint-patch": "1.3.3", 43 | "@types/chance": "1.1.3", 44 | "@types/node": "20.5.7", 45 | "@vitejs/plugin-vue": "4.3.3", 46 | "@vue/eslint-config-prettier": "8.0.0", 47 | "@vue/eslint-config-typescript": "11.0.3", 48 | "@vue/tsconfig": "0.4.0", 49 | "eslint": "8.48.0", 50 | "eslint-plugin-vue": "9.17.0", 51 | "prettier": "3.0.2", 52 | "sass": "1.66.1", 53 | "sass-loader": "13.3.2", 54 | "typescript": "5.1.6", 55 | "vite": "4.4.9", 56 | "vue-cli-plugin-vuetify": "2.5.8", 57 | "vue-tsc": "1.8.8", 58 | "vuetify-loader": "^2.0.0-alpha.9" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /dashboard/public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /dashboard/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dashboard/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /dashboard/src/assets/images/astrbot_logo_mini.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstrBotDevs/AstrBot/9147cab75bacf5d7f81548d8b209faac13a77d32/dashboard/src/assets/images/astrbot_logo_mini.webp -------------------------------------------------------------------------------- /dashboard/src/assets/images/logo-normal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 30 | 31 | 33 | 34 | 35 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /dashboard/src/assets/images/logo-waifu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstrBotDevs/AstrBot/9147cab75bacf5d7f81548d8b209faac13a77d32/dashboard/src/assets/images/logo-waifu.png -------------------------------------------------------------------------------- /dashboard/src/components/ConfirmDialog.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 45 | -------------------------------------------------------------------------------- /dashboard/src/components/shared/Logo.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 21 | 22 | 73 | -------------------------------------------------------------------------------- /dashboard/src/components/shared/UiParentCard.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 21 | -------------------------------------------------------------------------------- /dashboard/src/components/shared/WaitingForRestart.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /dashboard/src/config.ts: -------------------------------------------------------------------------------- 1 | export type ConfigProps = { 2 | Sidebar_drawer: boolean; 3 | Customizer_drawer: boolean; 4 | mini_sidebar: boolean; 5 | fontTheme: string; 6 | uiTheme: string; 7 | inputBg: boolean; 8 | }; 9 | 10 | function checkUITheme() { 11 | const theme = localStorage.getItem("uiTheme"); 12 | console.log('memorized theme: ', theme); 13 | if (!theme || !(['PurpleTheme', 'PurpleThemeDark'].includes(theme))) { 14 | localStorage.setItem("uiTheme", "PurpleTheme"); 15 | return 'PurpleTheme'; 16 | } else return theme; 17 | } 18 | 19 | const config: ConfigProps = { 20 | Sidebar_drawer: true, 21 | Customizer_drawer: false, 22 | mini_sidebar: false, 23 | fontTheme: 'Roboto', 24 | uiTheme: checkUITheme(), 25 | inputBg: false 26 | }; 27 | 28 | export default config; 29 | -------------------------------------------------------------------------------- /dashboard/src/layouts/blank/BlankLayout.vue: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /dashboard/src/layouts/full/FullLayout.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 27 | -------------------------------------------------------------------------------- /dashboard/src/layouts/full/vertical-sidebar/NavItem.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 36 | -------------------------------------------------------------------------------- /dashboard/src/layouts/full/vertical-sidebar/sidebarItem.ts: -------------------------------------------------------------------------------- 1 | export interface menu { 2 | header?: string; 3 | title?: string; 4 | icon?: string; 5 | to?: string; 6 | divider?: boolean; 7 | chip?: string; 8 | chipColor?: string; 9 | chipVariant?: string; 10 | chipIcon?: string; 11 | children?: menu[]; 12 | disabled?: boolean; 13 | type?: string; 14 | subCaption?: string; 15 | } 16 | 17 | const sidebarItem: menu[] = [ 18 | { 19 | title: '统计', 20 | icon: 'mdi-view-dashboard', 21 | to: '/dashboard/default' 22 | }, 23 | { 24 | title: '消息平台', 25 | icon: 'mdi-message-processing', 26 | to: '/platforms', 27 | }, 28 | { 29 | title: '服务提供商', 30 | icon: 'mdi-creation', 31 | to: '/providers', 32 | }, 33 | { 34 | title: 'MCP', 35 | icon: 'mdi-function-variant', 36 | to: '/tool-use' 37 | }, 38 | { 39 | title: '配置文件', 40 | icon: 'mdi-cog', 41 | to: '/config', 42 | }, 43 | { 44 | title: '插件管理', 45 | icon: 'mdi-puzzle', 46 | to: '/extension' 47 | }, 48 | { 49 | title: '插件市场', 50 | icon: 'mdi-storefront', 51 | to: '/extension-marketplace' 52 | }, 53 | { 54 | title: '聊天', 55 | icon: 'mdi-chat', 56 | to: '/chat' 57 | }, 58 | { 59 | title: '对话数据库', 60 | icon: 'mdi-database', 61 | to: '/conversation' 62 | }, 63 | { 64 | title: '控制台', 65 | icon: 'mdi-console', 66 | to: '/console' 67 | }, 68 | { 69 | title: 'Alkaid', 70 | icon: 'mdi-test-tube', 71 | to: '/alkaid' 72 | }, 73 | { 74 | title: '关于', 75 | icon: 'mdi-information', 76 | to: '/about' 77 | }, 78 | // { 79 | // title: 'Project ATRI', 80 | // icon: 'mdi-grain', 81 | // to: '/project-atri' 82 | // }, 83 | ]; 84 | 85 | export default sidebarItem; 86 | -------------------------------------------------------------------------------- /dashboard/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import { createPinia } from 'pinia'; 3 | import App from './App.vue'; 4 | import { router } from './router'; 5 | import vuetify from './plugins/vuetify'; 6 | import confirmPlugin from './plugins/confirmPlugin'; 7 | import '@/scss/style.scss'; 8 | import VueApexCharts from 'vue3-apexcharts'; 9 | 10 | import print from 'vue3-print-nb'; 11 | import { loader } from '@guolao/vue-monaco-editor' 12 | import axios from 'axios'; 13 | 14 | const app = createApp(App); 15 | app.use(router); 16 | app.use(createPinia()); 17 | app.use(print); 18 | app.use(VueApexCharts); 19 | app.use(vuetify); 20 | app.use(confirmPlugin); 21 | app.mount('#app'); 22 | 23 | 24 | axios.interceptors.request.use((config) => { 25 | const token = localStorage.getItem('token'); 26 | if (token) { 27 | config.headers['Authorization'] = `Bearer ${token}`; 28 | } 29 | return config; 30 | }); 31 | 32 | loader.config({ 33 | paths: { 34 | vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.43.0/min/vs', 35 | }, 36 | }) -------------------------------------------------------------------------------- /dashboard/src/plugins/confirmPlugin.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue"; 2 | import { h, render } from "vue"; 3 | import ConfirmDialog from "@/components/ConfirmDialog.vue"; 4 | 5 | export default { 6 | install(app: App) { 7 | const mountNode = document.createElement("div"); 8 | document.body.appendChild(mountNode); 9 | 10 | const vNode = h(ConfirmDialog); 11 | vNode.appContext = app._context; 12 | render(vNode, mountNode); 13 | 14 | const confirm = (options: { title?: string; message?: string }) => { 15 | return new Promise((resolve) => { 16 | vNode.component?.exposed?.open(options).then(resolve); // ✅ 确保返回 `Promise` 17 | }); 18 | }; 19 | 20 | app.config.globalProperties.$confirm = confirm; 21 | app.provide("$confirm", confirm); 22 | }, 23 | }; -------------------------------------------------------------------------------- /dashboard/src/plugins/vuetify.ts: -------------------------------------------------------------------------------- 1 | import { createVuetify } from 'vuetify'; 2 | import '@mdi/font/css/materialdesignicons.css'; 3 | import * as components from 'vuetify/components'; 4 | import * as directives from 'vuetify/directives'; 5 | import { PurpleTheme } from '@/theme/LightTheme'; 6 | import { PurpleThemeDark } from "@/theme/DarkTheme"; 7 | 8 | export default createVuetify({ 9 | components, 10 | directives, 11 | 12 | theme: { 13 | defaultTheme: 'PurpleTheme', 14 | themes: { 15 | PurpleTheme, 16 | PurpleThemeDark 17 | } 18 | }, 19 | defaults: { 20 | VBtn: {}, 21 | VCard: { 22 | rounded: 'md' 23 | }, 24 | VTextField: { 25 | rounded: 'lg' 26 | }, 27 | VTooltip: { 28 | // set v-tooltip default location to top 29 | location: 'top' 30 | } 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /dashboard/src/router/AuthRoutes.ts: -------------------------------------------------------------------------------- 1 | const AuthRoutes = { 2 | path: '/auth', 3 | component: () => import('@/layouts/blank/BlankLayout.vue'), 4 | meta: { 5 | requiresAuth: false 6 | }, 7 | children: [ 8 | { 9 | name: 'Login', 10 | path: '/auth/login', 11 | component: () => import('@/views/authentication/auth/LoginPage.vue') 12 | } 13 | ] 14 | }; 15 | 16 | export default AuthRoutes; 17 | -------------------------------------------------------------------------------- /dashboard/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router'; 2 | import MainRoutes from './MainRoutes'; 3 | import AuthRoutes from './AuthRoutes'; 4 | import { useAuthStore } from '@/stores/auth'; 5 | 6 | export const router = createRouter({ 7 | history: createWebHistory(import.meta.env.BASE_URL), 8 | routes: [ 9 | MainRoutes, 10 | AuthRoutes 11 | ] 12 | }); 13 | 14 | interface AuthStore { 15 | username: string; 16 | returnUrl: string | null; 17 | login(username: string, password: string): Promise; 18 | logout(): void; 19 | has_token(): boolean; 20 | } 21 | 22 | router.beforeEach(async (to, from, next) => { 23 | const publicPages = ['/auth/login']; 24 | const authRequired = !publicPages.includes(to.path); 25 | const auth: AuthStore = useAuthStore(); 26 | 27 | // 如果用户已登录且试图访问登录页面,则重定向到首页或之前尝试访问的页面 28 | if (to.path === '/auth/login' && auth.has_token()) { 29 | return next(auth.returnUrl || '/'); 30 | } 31 | 32 | if (to.matched.some((record) => record.meta.requiresAuth)) { 33 | if (authRequired && !auth.has_token()) { 34 | auth.returnUrl = to.fullPath; 35 | return next('/auth/login'); 36 | } else next(); 37 | } else { 38 | next(); 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /dashboard/src/scss/_override.scss: -------------------------------------------------------------------------------- 1 | html { 2 | .bg-success { 3 | color: white !important; 4 | } 5 | } 6 | 7 | .v-row + .v-row { 8 | margin-top: 0px; 9 | } 10 | 11 | .v-divider { 12 | opacity: 1; 13 | border-color: rgba(var(--v-theme-borderLight), 0.36); 14 | } 15 | 16 | .v-selection-control { 17 | flex: unset; 18 | } 19 | 20 | .customizer-btn .icon { 21 | animation: progress-circular-rotate 1.4s linear infinite; 22 | transform-origin: center center; 23 | transition: all 0.2s ease-in-out; 24 | } 25 | 26 | .no-spacer { 27 | .v-list-item__spacer { 28 | display: none !important; 29 | } 30 | } 31 | 32 | @keyframes progress-circular-rotate { 33 | 100% { 34 | transform: rotate(270deg); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /dashboard/src/scss/components/_VButtons.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Light Buttons 3 | // 4 | 5 | .v-btn { 6 | &.bg-lightsecondary { 7 | &:hover, 8 | &:active, 9 | &:focus { 10 | background-color: rgb(var(--v-theme-secondary)) !important; 11 | color: $white !important; 12 | } 13 | } 14 | } 15 | 16 | .v-btn { 17 | text-transform: capitalize; 18 | letter-spacing: $btn-letter-spacing; 19 | } 20 | .v-btn--icon.v-btn--density-default { 21 | width: calc(var(--v-btn-height) + 6px); 22 | height: calc(var(--v-btn-height) + 6px); 23 | } 24 | -------------------------------------------------------------------------------- /dashboard/src/scss/components/_VCard.scss: -------------------------------------------------------------------------------- 1 | // Outline Card 2 | .v-card--variant-outlined { 3 | border-color: rgba(var(--v-theme-borderLight), 0.36); 4 | .v-divider { 5 | border-color: rgba(var(--v-theme-borderLight), 0.36); 6 | } 7 | } 8 | 9 | .v-card-text { 10 | padding: $card-text-spacer; 11 | } 12 | 13 | .v-card { 14 | width: 100%; 15 | overflow: visible; 16 | &.withbg { 17 | background-color: rgb(var(--v-theme-background)); 18 | } 19 | &.overflow-hidden { 20 | overflow: hidden; 21 | } 22 | } 23 | 24 | .v-card-item { 25 | padding: $card-item-spacer-xy; 26 | } 27 | -------------------------------------------------------------------------------- /dashboard/src/scss/components/_VField.scss: -------------------------------------------------------------------------------- 1 | .v-field--variant-outlined .v-field__outline__start.v-locale--is-ltr, 2 | .v-locale--is-ltr .v-field--variant-outlined .v-field__outline__start { 3 | border-radius: $border-radius-root 0 0 $border-radius-root; 4 | } 5 | 6 | .v-field--variant-outlined .v-field__outline__end.v-locale--is-ltr, 7 | .v-locale--is-ltr .v-field--variant-outlined .v-field__outline__end { 8 | border-radius: 0 $border-radius-root $border-radius-root 0; 9 | } 10 | -------------------------------------------------------------------------------- /dashboard/src/scss/components/_VInput.scss: -------------------------------------------------------------------------------- 1 | .v-input--density-default, 2 | .v-field--variant-solo, 3 | .v-field--variant-filled { 4 | --v-input-control-height: 51px; 5 | --v-input-padding-top: 14px; 6 | } 7 | .v-input--density-comfortable { 8 | --v-input-control-height: 56px; 9 | --v-input-padding-top: 17px; 10 | } 11 | .v-label { 12 | font-size: 0.975rem; 13 | } 14 | .v-switch .v-label, 15 | .v-checkbox .v-label { 16 | opacity: 1; 17 | } 18 | -------------------------------------------------------------------------------- /dashboard/src/scss/components/_VNavigationDrawer.scss: -------------------------------------------------------------------------------- 1 | .v-navigation-drawer__scrim.fade-transition-leave-to { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /dashboard/src/scss/components/_VShadow.scss: -------------------------------------------------------------------------------- 1 | .elevation-10 { 2 | box-shadow: $box-shadow !important; 3 | } 4 | -------------------------------------------------------------------------------- /dashboard/src/scss/components/_VTabs.scss: -------------------------------------------------------------------------------- 1 | .theme-tab { 2 | &.v-tabs { 3 | .v-tab { 4 | border-radius: $border-radius-root !important; 5 | min-width: auto !important; 6 | &.v-slide-group-item--active { 7 | background: rgb(var(--v-theme-primary)); 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /dashboard/src/scss/components/_VTextField.scss: -------------------------------------------------------------------------------- 1 | .v-text-field input { 2 | font-size: 0.875rem; 3 | } 4 | .v-input--density-default { 5 | .v-field__input { 6 | min-height: 51px; 7 | } 8 | } 9 | 10 | .v-field__outline { 11 | color: rgb(var(--v-theme-inputBorder)); 12 | } 13 | .inputWithbg { 14 | .v-field--variant-outlined { 15 | background-color: rgba(0, 0, 0, 0.025); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /dashboard/src/scss/layout/_container.scss: -------------------------------------------------------------------------------- 1 | html { 2 | overflow-y: auto; 3 | } 4 | .v-main { 5 | margin-right: 20px; 6 | } 7 | @media (max-width: 1279px) { 8 | .v-main { 9 | margin: 0 10px; 10 | } 11 | } 12 | .spacer { 13 | padding: 100px 0; 14 | } 15 | @media (max-width: 800px) { 16 | .spacer { 17 | padding: 40px 0; 18 | } 19 | } 20 | 21 | .page-wrapper { 22 | min-height: calc(100vh - 100px); 23 | padding: 15px; 24 | border-radius: $border-radius-root; 25 | background: rgb(var(--v-theme-containerBg)); 26 | } 27 | $sizes: ( 28 | 'display-1': 44px, 29 | 'display-2': 40px, 30 | 'display-3': 30px, 31 | 'h1': 36px, 32 | 'h2': 30px, 33 | 'h3': 21px, 34 | 'h4': 18px, 35 | 'h5': 16px, 36 | 'h6': 14px, 37 | 'text-8': 8px, 38 | 'text-10': 10px, 39 | 'text-13': 13px, 40 | 'text-18': 18px, 41 | 'text-20': 20px, 42 | 'text-24': 24px, 43 | 'body-text-1': 10px 44 | ); 45 | 46 | @each $pixel, $size in $sizes { 47 | .#{$pixel} { 48 | font-size: $size; 49 | line-height: $size + 10; 50 | } 51 | } 52 | 53 | .customizer-btn { 54 | position: fixed; 55 | top: 25%; 56 | right: 10px; 57 | border-radius: 50% 50% 4px; 58 | .icon { 59 | animation: progress-circular-rotate 1.4s linear infinite; 60 | transform-origin: center center; 61 | transition: all 0.2s ease-in-out; 62 | } 63 | } 64 | .w-100 { 65 | width: 100%; 66 | } 67 | 68 | .h-100vh { 69 | height: 100vh; 70 | } 71 | 72 | .gap-3 { 73 | gap: 16px; 74 | } 75 | 76 | .text-white { 77 | color: rgb(255, 255, 255) !important; 78 | } 79 | 80 | // font family 81 | 82 | body { 83 | .Poppins { 84 | font-family: 'Poppins', sans-serif !important; 85 | } 86 | 87 | .Inter { 88 | font-family: 'Inter', sans-serif !important; 89 | } 90 | } 91 | 92 | @keyframes blink { 93 | 50% { 94 | opacity: 0; 95 | } 96 | 100% { 97 | opacity: 1; 98 | } 99 | } 100 | @keyframes bounce { 101 | 0%, 102 | 20%, 103 | 53%, 104 | to { 105 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 106 | transform: translateZ(0); 107 | } 108 | 40%, 109 | 43% { 110 | animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); 111 | transform: translate3d(0, -5px, 0); 112 | } 113 | 70% { 114 | animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); 115 | transform: translate3d(0, -7px, 0); 116 | } 117 | 80% { 118 | transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 119 | transform: translateZ(0); 120 | } 121 | 90% { 122 | transform: translate3d(0, -2px, 0); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /dashboard/src/scss/layout/_sidebar.scss: -------------------------------------------------------------------------------- 1 | /*This is for the logo*/ 2 | .leftSidebar { 3 | border: 0px; 4 | box-shadow: none !important; 5 | } 6 | .listitem { 7 | height: calc(100vh - 100px); 8 | .v-list { 9 | color: rgb(var(--v-theme-secondaryText)); 10 | } 11 | .v-list-group__items .v-list-item, 12 | .v-list-item { 13 | border-radius: $border-radius-root; 14 | padding-inline-start: calc(12px + var(--indent-padding) / 2) !important; 15 | &:hover { 16 | color: rgb(var(--v-theme-secondary)); 17 | } 18 | } 19 | .v-list-item--density-default.v-list-item--one-line { 20 | min-height: 42px; 21 | } 22 | .leftPadding { 23 | margin-left: 4px; 24 | } 25 | } 26 | .v-navigation-drawer--rail { 27 | .scrollnavbar .v-list .v-list-group__items, 28 | .hide-menu { 29 | opacity: 1; 30 | } 31 | .leftPadding { 32 | margin-left: 0px; 33 | } 34 | } 35 | @media only screen and (min-width: 1170px) { 36 | .mini-sidebar { 37 | .logo { 38 | width: 90px; 39 | overflow: hidden; 40 | } 41 | .leftSidebar:hover { 42 | box-shadow: $box-shadow !important; 43 | } 44 | .v-navigation-drawer--expand-on-hover:hover { 45 | .logo { 46 | width: 100%; 47 | } 48 | .v-list .v-list-group__items, 49 | .hide-menu { 50 | opacity: 1; 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /dashboard/src/scss/pages/_dashboards.scss: -------------------------------------------------------------------------------- 1 | .bubble-shape { 2 | position: relative; 3 | &:before { 4 | content: ''; 5 | position: absolute; 6 | width: 210px; 7 | height: 210px; 8 | border-radius: 50%; 9 | top: -125px; 10 | right: -15px; 11 | opacity: 0.5; 12 | } 13 | &:after { 14 | content: ''; 15 | position: absolute; 16 | width: 210px; 17 | height: 210px; 18 | border-radius: 50%; 19 | top: -85px; 20 | right: -95px; 21 | } 22 | 23 | // &.bubble-primary-shape { 24 | // &::before { 25 | // background: rgb(var(--v-theme-darkprimary)); 26 | // } 27 | // &::after { 28 | // background: rgb(var(--v-theme-darkprimary)); 29 | // } 30 | // } 31 | 32 | // &.bubble-secondary-shape { 33 | // &::before { 34 | // background: rgb(var(--v-theme-darksecondary)); 35 | // } 36 | // &::after { 37 | // background: rgb(var(--v-theme-darksecondary)); 38 | // } 39 | // } 40 | } 41 | 42 | .z-1 { 43 | z-index: 1; 44 | position: relative; 45 | } 46 | .bubble-shape-sm { 47 | position: relative; 48 | &::before { 49 | content: ''; 50 | position: absolute; 51 | width: 210px; 52 | height: 210px; 53 | border-radius: 50%; 54 | top: -160px; 55 | right: -130px; 56 | } 57 | // &.bubble-primary { 58 | // &::before { 59 | // background: linear-gradient(140.9deg, rgb(var(--v-theme-lightprimary)) -14.02%, rgba(var(--v-theme-darkprimary), 0) 77.58%); 60 | // } 61 | // } 62 | &::after { 63 | content: ''; 64 | position: absolute; 65 | width: 210px; 66 | height: 210px; 67 | border-radius: 50%; 68 | top: -30px; 69 | right: -180px; 70 | } 71 | // &.bubble-primary { 72 | // &::after { 73 | // background: linear-gradient(210.04deg, rgb(var(--v-theme-lightprimary)) -50.94%, rgba(var(--v-theme-darkprimary), 0) 83.49%); 74 | // } 75 | // } 76 | 77 | // &.bubble-warning { 78 | // &::before { 79 | // background: linear-gradient(140.9deg, rgb(var(--v-theme-warning)) -14.02%, rgba(144, 202, 249, 0) 70.5%); 80 | // } 81 | // } 82 | 83 | // &.bubble-warning { 84 | // &::after { 85 | // background: linear-gradient(210.04deg, rgb(var(--v-theme-warning)) -50.94%, rgba(144, 202, 249, 0) 83.49%); 86 | // } 87 | // } 88 | } 89 | 90 | .rounded-square { 91 | width: 20px; 92 | height: 20px; 93 | } 94 | -------------------------------------------------------------------------------- /dashboard/src/scss/style.scss: -------------------------------------------------------------------------------- 1 | @import './variables'; 2 | @import 'vuetify/styles/main.sass'; 3 | @import './override'; 4 | @import './layout/container'; 5 | @import './layout/sidebar'; 6 | 7 | @import './components/VButtons'; 8 | @import './components/VCard'; 9 | @import './components/VField'; 10 | @import './components/VInput'; 11 | @import './components/VNavigationDrawer'; 12 | @import './components/VShadow'; 13 | @import './components/VTextField'; 14 | @import './components/VTabs'; 15 | 16 | @import './pages/dashboards'; 17 | -------------------------------------------------------------------------------- /dashboard/src/stores/auth.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { router } from '@/router'; 3 | import axios from 'axios'; 4 | 5 | export const useAuthStore = defineStore({ 6 | id: 'auth', 7 | state: () => ({ 8 | // @ts-ignore 9 | username: '', 10 | returnUrl: null 11 | }), 12 | actions: { 13 | async login(username: string, password: string): Promise { 14 | try { 15 | const res = await axios.post('/api/auth/login', { 16 | username: username, 17 | password: password 18 | }); 19 | 20 | if (res.data.status === 'error') { 21 | return Promise.reject(res.data.message); 22 | } 23 | 24 | this.username = res.data.data.username 25 | localStorage.setItem('user', this.username); 26 | localStorage.setItem('token', res.data.data.token); 27 | localStorage.setItem('change_pwd_hint', res.data.data?.change_pwd_hint); 28 | router.push(this.returnUrl || '/dashboard/default'); 29 | } catch (error) { 30 | return Promise.reject(error); 31 | } 32 | }, 33 | logout() { 34 | this.username = ''; 35 | localStorage.removeItem('username'); 36 | localStorage.removeItem('token'); 37 | router.push('/auth/login'); 38 | }, 39 | has_token(): boolean { 40 | return !!localStorage.getItem('token'); 41 | } 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /dashboard/src/stores/customizer.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import config from '@/config'; 3 | 4 | export const useCustomizerStore = defineStore({ 5 | id: 'customizer', 6 | state: () => ({ 7 | Sidebar_drawer: config.Sidebar_drawer, 8 | Customizer_drawer: config.Customizer_drawer, 9 | mini_sidebar: config.mini_sidebar, 10 | fontTheme: "Poppins", 11 | uiTheme: config.uiTheme, 12 | inputBg: config.inputBg 13 | }), 14 | 15 | getters: {}, 16 | actions: { 17 | SET_SIDEBAR_DRAWER() { 18 | this.Sidebar_drawer = !this.Sidebar_drawer; 19 | }, 20 | SET_MINI_SIDEBAR(payload: boolean) { 21 | this.mini_sidebar = payload; 22 | }, 23 | SET_FONT(payload: string) { 24 | this.fontTheme = payload; 25 | }, 26 | SET_UI_THEME(payload: string) { 27 | this.uiTheme = payload; 28 | localStorage.setItem("uiTheme", payload); 29 | }, 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /dashboard/src/theme/DarkTheme.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeTypes } from '@/types/themeTypes/ThemeType'; 2 | 3 | const PurpleThemeDark: ThemeTypes = { 4 | name: 'PurpleThemeDark', 5 | dark: true, 6 | variables: { 7 | 'border-color': '#1677ff', 8 | 'carousel-control-size': 10 9 | }, 10 | colors: { 11 | primary: '#1677ff', 12 | secondary: '#722ed1', 13 | info: '#03c9d7', 14 | success: '#52c41a', 15 | accent: '#FFAB91', 16 | warning: '#faad14', 17 | error: '#ff4d4f', 18 | lightprimary: '#eef2f6', 19 | lightsecondary: '#ede7f6', 20 | lightsuccess: '#b9f6ca', 21 | lighterror: '#f9d8d8', 22 | lightwarning: '#fff8e1', 23 | primaryText: '#ffffff', 24 | secondaryText: '#ffffffcc', 25 | darkprimary: '#1565c0', 26 | darksecondary: '#4527a0', 27 | borderLight: '#d0d0d0', 28 | border: '#333333ee', 29 | inputBorder: '#787878', 30 | containerBg: '#1a1a1a', 31 | surface: '#1f1f1f', 32 | 'on-surface-variant': '#000', 33 | facebook: '#4267b2', 34 | twitter: '#1da1f2', 35 | linkedin: '#0e76a8', 36 | gray100: '#cccccccc', 37 | primary200: '#90caf9', 38 | secondary200: '#b39ddb', 39 | background: '#111111', 40 | overlay: '#111111aa', 41 | codeBg: '#282833', 42 | code: '#ffffffdd' 43 | } 44 | }; 45 | 46 | export { PurpleThemeDark }; 47 | -------------------------------------------------------------------------------- /dashboard/src/theme/LightTheme.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeTypes } from '@/types/themeTypes/ThemeType'; 2 | 3 | const PurpleTheme: ThemeTypes = { 4 | name: 'PurpleTheme', 5 | dark: false, 6 | variables: { 7 | 'border-color': '#1e88e5', 8 | 'carousel-control-size': 10 9 | }, 10 | colors: { 11 | primary: '#1e88e5', 12 | secondary: '#5e35b1', 13 | info: '#03c9d7', 14 | success: '#00c853', 15 | accent: '#FFAB91', 16 | warning: '#ffc107', 17 | error: '#f44336', 18 | lightprimary: '#eef2f6', 19 | lightsecondary: '#ede7f6', 20 | lightsuccess: '#b9f6ca', 21 | lighterror: '#f9d8d8', 22 | lightwarning: '#fff8e1', 23 | primaryText: '#000000dd', 24 | secondaryText: '#000000aa', 25 | darkprimary: '#1565c0', 26 | darksecondary: '#4527a0', 27 | borderLight: '#d0d0d0', 28 | border: '#d0d0d0', 29 | inputBorder: '#787878', 30 | containerBg: '#eef2f6', 31 | surface: '#fff', 32 | 'on-surface-variant': '#fff', 33 | facebook: '#4267b2', 34 | twitter: '#1da1f2', 35 | linkedin: '#0e76a8', 36 | gray100: '#fafafacc', 37 | primary200: '#90caf9', 38 | secondary200: '#b39ddb', 39 | background: '#f9fafcf4', 40 | overlay: '#ffffffaa', 41 | codeBg: '#f5f0ff', 42 | code: '#673ab7' 43 | } 44 | }; 45 | 46 | export { PurpleTheme }; 47 | -------------------------------------------------------------------------------- /dashboard/src/types/themeTypes/ThemeType.ts: -------------------------------------------------------------------------------- 1 | export type ThemeTypes = { 2 | name: string; 3 | dark: boolean; 4 | variables?: object; 5 | colors: { 6 | primary?: string; 7 | secondary?: string; 8 | info?: string; 9 | success?: string; 10 | accent?: string; 11 | warning?: string; 12 | error?: string; 13 | lightprimary?: string; 14 | lightsecondary?: string; 15 | lightsuccess?: string; 16 | lighterror?: string; 17 | lightwarning?: string; 18 | darkprimary?: string; 19 | darksecondary?: string; 20 | primaryText?: string; 21 | secondaryText?: string; 22 | borderLight?: string; 23 | border?: string; 24 | inputBorder?: string; 25 | containerBg?: string; 26 | surface?: string; 27 | background?: string; 28 | overlay?: string; 29 | 'on-surface-variant'?: string; 30 | facebook?: string; 31 | twitter?: string; 32 | linkedin?: string; 33 | gray100?: string; 34 | primary200?: string; 35 | secondary200?: string; 36 | codeBg?: string; 37 | code?: string; 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /dashboard/src/types/vue3-print-nb.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'vue3-print-nb'; 2 | -------------------------------------------------------------------------------- /dashboard/src/types/vue_tabler_icon.d.ts: -------------------------------------------------------------------------------- 1 | import { VNodeChild } from 'vue'; 2 | declare module '@vue/runtime-dom' { 3 | export interface HTMLAttributes { 4 | $children?: VNodeChild; 5 | } 6 | export interface SVGAttributes { 7 | $children?: VNodeChild; 8 | strokeWidth?: string | number; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /dashboard/src/views/AlkaidPage.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 62 | 63 | -------------------------------------------------------------------------------- /dashboard/src/views/Settings.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | -------------------------------------------------------------------------------- /dashboard/src/views/alkaid/Other.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /dashboard/src/views/dashboards/default/components/OnlinePlatform.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 27 | 28 | -------------------------------------------------------------------------------- /dashboard/src/views/dashboards/default/components/RunningTime.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 32 | 33 | 90 | -------------------------------------------------------------------------------- /dashboard/src/views/dashboards/default/components/TotalMessage.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 36 | 37 | -------------------------------------------------------------------------------- /dashboard/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "src/types/.d.ts"], 4 | "compilerOptions": { 5 | "ignoreDeprecations": "5.0", 6 | "baseUrl": ".", 7 | "paths": { 8 | "@/*": ["./src/*"] 9 | }, 10 | "allowJs": true 11 | }, 12 | 13 | "references": [ 14 | { 15 | "path": "./tsconfig.vite-config.json" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /dashboard/tsconfig.vite-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.json", 3 | "include": ["vite.config.*"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "allowJs": true, 7 | "types": ["node"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /dashboard/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'url'; 2 | import { defineConfig } from 'vite'; 3 | import vue from '@vitejs/plugin-vue'; 4 | import vuetify from 'vite-plugin-vuetify'; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [ 9 | vue({ 10 | template: { 11 | compilerOptions: { 12 | isCustomElement: (tag) => ['v-list-recognize-title'].includes(tag) 13 | } 14 | } 15 | }), 16 | vuetify({ 17 | autoImport: true 18 | }) 19 | ], 20 | resolve: { 21 | alias: { 22 | '@': fileURLToPath(new URL('./src', import.meta.url)) 23 | } 24 | }, 25 | css: { 26 | preprocessorOptions: { 27 | scss: {} 28 | } 29 | }, 30 | build: { 31 | chunkSizeWarningLimit: 1024 * 1024 // Set the limit to 1 MB 32 | }, 33 | optimizeDeps: { 34 | exclude: ['vuetify'], 35 | entries: ['./src/**/*.vue'] 36 | }, 37 | server: { 38 | host: '0.0.0.0', 39 | port: 3000, 40 | proxy: { 41 | '/api': { 42 | target: 'http://localhost:6185/', 43 | changeOrigin: true, 44 | } 45 | } 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import asyncio 3 | import sys 4 | import mimetypes 5 | from astrbot.core.initial_loader import InitialLoader 6 | from astrbot.core import db_helper 7 | from astrbot.core import logger, LogManager, LogBroker 8 | from astrbot.core.config.default import VERSION 9 | from astrbot.core.utils.io import download_dashboard, get_dashboard_version 10 | 11 | # add parent path to sys.path 12 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 13 | 14 | logo_tmpl = r""" 15 | ___ _______.___________..______ .______ ______ .___________. 16 | / \ / | || _ \ | _ \ / __ \ | | 17 | / ^ \ | (----`---| |----`| |_) | | |_) | | | | | `---| |----` 18 | / /_\ \ \ \ | | | / | _ < | | | | | | 19 | / _____ \ .----) | | | | |\ \----.| |_) | | `--' | | | 20 | /__/ \__\ |_______/ |__| | _| `._____||______/ \______/ |__| 21 | 22 | """ 23 | 24 | 25 | def check_env(): 26 | if not (sys.version_info.major == 3 and sys.version_info.minor >= 10): 27 | logger.error("请使用 Python3.10+ 运行本项目。") 28 | exit() 29 | 30 | os.makedirs("data/config", exist_ok=True) 31 | os.makedirs("data/plugins", exist_ok=True) 32 | os.makedirs("data/temp", exist_ok=True) 33 | 34 | # workaround for issue #181 35 | mimetypes.add_type("text/javascript", ".js") 36 | mimetypes.add_type("text/javascript", ".mjs") 37 | mimetypes.add_type("application/json", ".json") 38 | 39 | 40 | async def check_dashboard_files(): 41 | """下载管理面板文件""" 42 | 43 | v = await get_dashboard_version() 44 | if v is not None: 45 | # has file 46 | if v == f"v{VERSION}": 47 | logger.info("管理面板文件已是最新。") 48 | else: 49 | logger.warning( 50 | "检测到管理面板有更新。可以使用 /dashboard_update 命令更新。" 51 | ) 52 | return 53 | 54 | logger.info( 55 | "开始下载管理面板文件...高峰期(晚上)可能导致较慢的速度。如多次下载失败,请前往 https://github.com/Soulter/AstrBot/releases/latest 下载 dist.zip,并将其中的 dist 文件夹解压至 data 目录下。" 56 | ) 57 | 58 | try: 59 | await download_dashboard() 60 | except Exception as e: 61 | logger.critical(f"下载管理面板文件失败: {e}。") 62 | return 63 | 64 | logger.info("管理面板下载完成。") 65 | 66 | 67 | if __name__ == "__main__": 68 | check_env() 69 | 70 | # start log broker 71 | log_broker = LogBroker() 72 | LogManager.set_queue_handler(logger, log_broker) 73 | 74 | # check dashboard files 75 | asyncio.run(check_dashboard_files()) 76 | 77 | db = db_helper 78 | 79 | # print logo 80 | logger.info(logo_tmpl) 81 | 82 | core_lifecycle = InitialLoader(db, log_broker) 83 | asyncio.run(core_lifecycle.start()) 84 | -------------------------------------------------------------------------------- /packages/python_interpreter/requirements.txt: -------------------------------------------------------------------------------- 1 | aiodocker -------------------------------------------------------------------------------- /packages/python_interpreter/shared/api.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def _get_magic_code(): 5 | """防止注入攻击""" 6 | return os.getenv("MAGIC_CODE") 7 | 8 | 9 | def send_text(text: str): 10 | print(f"[ASTRBOT_TEXT_OUTPUT#{_get_magic_code()}]: {text}") 11 | 12 | 13 | def send_image(image_path: str): 14 | if not os.path.exists(image_path): 15 | raise Exception(f"Image file not found: {image_path}") 16 | print(f"[ASTRBOT_IMAGE_OUTPUT#{_get_magic_code()}]: {image_path}") 17 | 18 | 19 | def send_file(file_path: str): 20 | if not os.path.exists(file_path): 21 | raise Exception(f"File not found: {file_path}") 22 | print(f"[ASTRBOT_FILE_OUTPUT#{_get_magic_code()}]: {file_path}") 23 | -------------------------------------------------------------------------------- /packages/web_searcher/engines/bing.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from . import SearchEngine, SearchResult 3 | from . import USER_AGENT_BING 4 | 5 | 6 | class Bing(SearchEngine): 7 | def __init__(self) -> None: 8 | super().__init__() 9 | self.base_urls = ["https://cn.bing.com", "https://www.bing.com"] 10 | self.headers.update({"User-Agent": USER_AGENT_BING}) 11 | 12 | def _set_selector(self, selector: str): 13 | selectors = { 14 | "url": "div.b_attribution cite", 15 | "title": "h2", 16 | "text": "p", 17 | "links": "ol#b_results > li.b_algo", 18 | "next": 'div#b_content nav[role="navigation"] a.sb_pagN', 19 | } 20 | return selectors[selector] 21 | 22 | async def _get_next_page(self, query) -> str: 23 | # if self.page == 1: 24 | # await self._get_html(self.base_url) 25 | for base_url in self.base_urls: 26 | try: 27 | url = f"{base_url}/search?q={query}" 28 | return await self._get_html(url, None) 29 | except Exception as _: 30 | self.base_url = base_url 31 | continue 32 | raise Exception("Bing search failed") 33 | 34 | async def search(self, query: str, num_results: int) -> List[SearchResult]: 35 | results = await super().search(query, num_results) 36 | for result in results: 37 | if not isinstance(result.url, str): 38 | result.url = result.url.text 39 | 40 | return results 41 | -------------------------------------------------------------------------------- /packages/web_searcher/engines/google.py: -------------------------------------------------------------------------------- 1 | import os 2 | from googlesearch import search 3 | 4 | from . import SearchEngine, SearchResult 5 | 6 | from typing import List 7 | 8 | 9 | class Google(SearchEngine): 10 | def __init__(self) -> None: 11 | super().__init__() 12 | self.proxy = os.environ.get("https_proxy") 13 | 14 | async def search(self, query: str, num_results: int) -> List[SearchResult]: 15 | results = [] 16 | try: 17 | ls = search( 18 | query, 19 | advanced=True, 20 | num_results=num_results, 21 | timeout=3, 22 | proxy=self.proxy, 23 | ) 24 | for i in ls: 25 | results.append( 26 | SearchResult(title=i.title, url=i.url, snippet=i.description) 27 | ) 28 | except Exception as e: 29 | raise e 30 | return results 31 | -------------------------------------------------------------------------------- /packages/web_searcher/engines/sogo.py: -------------------------------------------------------------------------------- 1 | import random 2 | import re 3 | from bs4 import BeautifulSoup 4 | from . import SearchEngine, SearchResult 5 | from . import USER_AGENTS 6 | 7 | from typing import List 8 | 9 | 10 | class Sogo(SearchEngine): 11 | def __init__(self) -> None: 12 | super().__init__() 13 | self.base_url = "https://www.sogou.com" 14 | self.headers["User-Agent"] = random.choice(USER_AGENTS) 15 | 16 | def _set_selector(self, selector: str): 17 | selectors = { 18 | "url": "h3 > a", 19 | "title": "h3", 20 | "text": "", 21 | "links": "div.results > div.vrwrap:not(.middle-better-hintBox)", 22 | "next": "", 23 | } 24 | return selectors[selector] 25 | 26 | async def _get_next_page(self, query) -> str: 27 | url = f"{self.base_url}/web?query={query}" 28 | return await self._get_html(url, None) 29 | 30 | async def search(self, query: str, num_results: int) -> List[SearchResult]: 31 | results = await super().search(query, num_results) 32 | for result in results: 33 | result.url = result.url.get("href") 34 | if result.url.startswith("/link?"): 35 | result.url = self.base_url + result.url 36 | result.url = await self._parse_url(result.url) 37 | return results 38 | 39 | async def _parse_url(self, url) -> str: 40 | html = await self._get_html(url) 41 | soup = BeautifulSoup(html, "html.parser") 42 | script = soup.find("script") 43 | if script: 44 | url = re.search(r'window.location.replace\("(.+?)"\)', script.string).group( 45 | 1 46 | ) 47 | return url 48 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "AstrBot" 3 | version = "3.5.13" 4 | description = "易上手的多平台 LLM 聊天机器人及开发框架" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | dependencies = [ 8 | "aiocqhttp>=1.4.4", 9 | "aiodocker>=0.24.0", 10 | "aiohttp>=3.11.18", 11 | "aiosqlite>=0.21.0", 12 | "anthropic>=0.51.0", 13 | "apscheduler>=3.11.0", 14 | "beautifulsoup4>=4.13.4", 15 | "certifi>=2025.4.26", 16 | "chardet~=5.1.0", 17 | "colorlog>=6.9.0", 18 | "cryptography>=44.0.3", 19 | "dashscope>=1.23.2", 20 | "defusedxml>=0.7.1", 21 | "dingtalk-stream>=0.22.1", 22 | "docstring-parser>=0.16", 23 | "faiss-cpu>=1.10.0", 24 | "filelock>=3.18.0", 25 | "google-genai>=1.14.0", 26 | "googlesearch-python>=1.3.0", 27 | "lark-oapi>=1.4.15", 28 | "lxml-html-clean>=0.4.2", 29 | "mcp>=1.8.0", 30 | "nh3>=0.2.21", 31 | "openai>=1.78.0", 32 | "ormsgpack>=1.9.1", 33 | "pillow>=11.2.1", 34 | "pip>=25.1.1", 35 | "psutil>=5.8.0", 36 | "pydantic~=2.10.3", 37 | "pydub>=0.25.1", 38 | "pyjwt>=2.10.1", 39 | "python-telegram-bot>=22.0", 40 | "qq-botpy>=1.2.1", 41 | "quart>=0.20.0", 42 | "readability-lxml>=0.8.4.1", 43 | "silk-python>=0.2.6", 44 | "telegramify-markdown>=0.5.1", 45 | "watchfiles>=1.0.5", 46 | "websockets>=15.0.1", 47 | "wechatpy>=1.8.18", 48 | ] 49 | 50 | [project.scripts] 51 | astrbot = "astrbot.cli.__main__:cli" 52 | 53 | [build-system] 54 | requires = ["hatchling", "uv-dynamic-versioning"] 55 | build-backend = "hatchling.build" 56 | 57 | [tool.ruff] 58 | exclude = [ 59 | "astrbot/core/utils/t2i/local_strategy.py", 60 | "astrbot/api/all.py", 61 | ] 62 | line-length = 88 63 | lint.ignore = [ 64 | "F403", 65 | "F405", 66 | "E501", 67 | "ASYNC230" # TODO: handle ASYNC230 in AstrBot 68 | ] 69 | lint.select = [ 70 | "F", # Pyflakes 71 | "W", # pycodestyle warnings 72 | "E", # pycodestyle errors 73 | "ASYNC", # flake8-async 74 | "C4", # flake8-comprehensions 75 | "Q", # flake8-quotes 76 | ] 77 | target-version = "py310" 78 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | pydantic~=2.10.3 3 | psutil>=5.8.0 4 | openai 5 | anthropic 6 | qq-botpy 7 | chardet~=5.1.0 8 | Pillow 9 | beautifulsoup4 10 | googlesearch-python 11 | readability-lxml 12 | quart 13 | lxml_html_clean 14 | colorlog 15 | aiocqhttp 16 | pyjwt 17 | apscheduler 18 | docstring_parser 19 | aiodocker 20 | silk-python 21 | lark-oapi 22 | ormsgpack 23 | cryptography 24 | dashscope 25 | python-telegram-bot 26 | wechatpy 27 | dingtalk-stream 28 | defusedxml 29 | mcp 30 | certifi 31 | pip 32 | telegramify-markdown 33 | google-genai 34 | click 35 | filelock 36 | watchfiles 37 | websockets 38 | faiss-cpu 39 | aiosqlite 40 | nh3 -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import pytest 4 | from unittest import mock 5 | from main import check_env, check_dashboard_files 6 | 7 | 8 | class _version_info: 9 | def __init__(self, major, minor): 10 | self.major = major 11 | self.minor = minor 12 | 13 | 14 | def test_check_env(monkeypatch): 15 | version_info_correct = _version_info(3, 10) 16 | version_info_wrong = _version_info(3, 9) 17 | monkeypatch.setattr(sys, "version_info", version_info_correct) 18 | with mock.patch("os.makedirs") as mock_makedirs: 19 | check_env() 20 | mock_makedirs.assert_any_call("data/config", exist_ok=True) 21 | mock_makedirs.assert_any_call("data/plugins", exist_ok=True) 22 | mock_makedirs.assert_any_call("data/temp", exist_ok=True) 23 | 24 | monkeypatch.setattr(sys, "version_info", version_info_wrong) 25 | with pytest.raises(SystemExit): 26 | check_env() 27 | 28 | 29 | @pytest.mark.asyncio 30 | async def test_check_dashboard_files(monkeypatch): 31 | monkeypatch.setattr(os.path, "exists", lambda x: False) 32 | 33 | async def mock_get(*args, **kwargs): 34 | class MockResponse: 35 | status = 200 36 | 37 | async def read(self): 38 | return b"content" 39 | 40 | return MockResponse() 41 | 42 | with mock.patch("aiohttp.ClientSession.get", new=mock_get): 43 | with mock.patch("builtins.open", mock.mock_open()) as mock_file: 44 | with mock.patch("zipfile.ZipFile.extractall") as mock_extractall: 45 | 46 | async def mock_aenter(_): 47 | await check_dashboard_files() 48 | mock_file.assert_called_once_with("data/dashboard.zip", "wb") 49 | mock_extractall.assert_called_once() 50 | 51 | async def mock_aexit(obj, exc_type, exc, tb): 52 | return 53 | 54 | mock_extractall.__aenter__ = mock_aenter 55 | mock_extractall.__aexit__ = mock_aexit 56 | --------------------------------------------------------------------------------