├── .gitattributes ├── .github └── workflows │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── config.example.yaml ├── docs ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vitepress │ ├── config.mts │ ├── env.d.ts │ └── theme │ │ ├── Layout.vue │ │ ├── components │ │ ├── Contributors.vue │ │ ├── GitHubUser.vue │ │ └── Sparkler.vue │ │ ├── index.css │ │ └── index.ts ├── LICENSE ├── env.d.ts ├── guide │ ├── concepts.md │ ├── configuration.md │ ├── deployment.md │ ├── dify-integration.md │ ├── faq.md │ ├── mcp-integration.md │ ├── message-handling.md │ ├── plugins.md │ ├── quick-start.md │ ├── rpa-operations.md │ └── troubleshooting.md ├── index.md ├── package-lock.json ├── package.json ├── public │ ├── author.jpg │ ├── favicon.ico │ ├── group1.jpg │ ├── logo.png │ ├── vercel.json │ └── zs.jpg ├── sponsor.md └── tsconfig.json ├── examples └── simple-bot │ └── bot.py ├── pyproject.toml ├── scripts └── update_version_and_tag.py └── src └── omni_bot_sdk ├── __init__.py ├── bot.py ├── clients ├── dify_client.py ├── minio_client.py └── mqtt_client.py ├── common ├── __init__.py ├── config.py ├── exceptions.py └── queues.py ├── mcp ├── __init__.py ├── app.py ├── dispatchers.py └── protocols.py ├── models.py ├── plugins ├── __init__.py ├── core │ ├── __init__.py │ ├── block_empty_room_plugin.py │ ├── image_aes_plugin.py │ ├── plugin_interface.py │ └── self_msg_plugin.py ├── interface.py ├── plugin_manager.py └── task.md ├── rpa ├── __init__.py ├── action_handlers │ ├── __init__.py │ ├── announcement_handler.py │ ├── base_handler.py │ ├── download_file_handler.py │ ├── download_image_handler.py │ ├── download_video_handler.py │ ├── forward_message_handler.py │ ├── functional │ │ ├── __init__.py │ │ ├── invite2room_handler.py │ │ ├── new_friend_handler.py │ │ └── send_pyq_handler.py │ ├── leave_room_handler.py │ ├── mixins │ │ ├── group_operations_mixin.py │ │ └── window_operations_mixin.py │ ├── pat_handler.py │ ├── remove_room_member_handler.py │ ├── rename_name_in_room_handler.py │ ├── rename_room_name_handler.py │ ├── rename_room_remark_handler.py │ ├── send_file_handler.py │ ├── send_image_handler.py │ ├── send_text_message_handler.py │ └── switch_conversation_handler.py ├── controller.py ├── image_processor.py ├── input_handler.py ├── message_sender.py ├── ocr_processor.py ├── ui_helper.py └── window_manager.py ├── services ├── __init__.py ├── core │ ├── __init__.py │ ├── async_plugin_runner.py │ ├── database_service.cp312-win_amd64.pyd │ ├── message_factory_service.py │ ├── message_service.py │ ├── mqtt_service.py │ ├── processor_service.py │ ├── rpa_service.py │ └── user_service.py └── functional │ ├── __init__.py │ ├── dat_decrypt_service.py │ ├── new_friend_check_service.py │ └── weixin_status_service.py ├── utils ├── __init__.py ├── fuck_zxl.cp312-win_amd64.pyd ├── helpers.py ├── logging_setup.py ├── mouse.py └── size_config.py ├── weixin ├── __init__.py ├── message_classes.py ├── message_factory.py └── parser │ ├── __init__.py │ ├── audio_parser.py │ ├── emoji_parser.py │ ├── file_parser.py │ ├── link_parser.py │ └── util │ ├── __init__.py │ ├── common.py │ └── protocbuf │ ├── __init__.py │ ├── contact.proto │ ├── contact_pb2.py │ ├── emoji_desc.proto │ ├── emoji_desc_pb2.py │ ├── file_info.proto │ ├── file_info_pb2.py │ ├── msg.proto │ ├── msg_pb2.py │ ├── packed_info_data.proto │ ├── packed_info_data_img.proto │ ├── packed_info_data_img2.proto │ ├── packed_info_data_img2_pb2.py │ ├── packed_info_data_img_pb2.py │ ├── packed_info_data_merged.proto │ ├── packed_info_data_merged_pb2.py │ ├── packed_info_data_pb2.py │ ├── readme.md │ ├── roomdata.proto │ └── roomdata_pb2.py └── yolo ├── get_model_path.py └── models ├── .gitkeep └── msg_rec.pt /.gitattributes: -------------------------------------------------------------------------------- 1 | # auto generated by UGit 2 | *.pyc filter=lfs diff=lfs merge=binary -text 3 | *.whl filter=lfs diff=lfs merge=binary -text 4 | *.gz filter=lfs diff=lfs merge=binary -text 5 | 6 | # auto generated by UGit 7 | *.pyd filter=lfs diff=lfs merge=binary -text 8 | *.dat filter=lfs diff=lfs merge=binary -text 9 | 10 | 11 | # auto generated by UGit 12 | *.png filter=lfs diff=lfs merge=binary -text 13 | 14 | 15 | # auto generated by UGit 16 | 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/release.yml 2 | name: Release Workflow 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | # Job 1: Release Please 11 | release-please: 12 | runs-on: ubuntu-latest 13 | outputs: 14 | release_created: ${{ steps.release.outputs.release_created }} 15 | tag_name: ${{ steps.release.outputs.tag_name }} 16 | permissions: 17 | contents: write 18 | pull-requests: write 19 | issues: write 20 | steps: 21 | - uses: googleapis/release-please-action@v4 22 | id: release 23 | with: 24 | token: ${{ secrets.GITHUB_TOKEN }} 25 | release-type: python 26 | 27 | # Job 2: Build on Windows 28 | build: 29 | needs: release-please 30 | if: ${{ needs.release-please.outputs.release_created }} 31 | runs-on: windows-latest 32 | steps: 33 | - name: Checkout code 34 | uses: actions/checkout@v4 35 | with: 36 | lfs: true 37 | - name: Set up Python 38 | uses: actions/setup-python@v5 39 | with: 40 | python-version: '3.12' 41 | - name: Install build dependencies 42 | run: | 43 | python -m pip install --upgrade pip 44 | pip install build 45 | - name: Build package 46 | run: python -m build 47 | - name: Upload artifact 48 | uses: actions/upload-artifact@v4 49 | with: 50 | name: python-package 51 | path: dist/ 52 | 53 | # Job 3: Publish from Ubuntu 54 | publish: 55 | # 明确依赖 release-please 和 build 两个 job 56 | needs: [release-please, build] 57 | if: ${{ needs.release-please.outputs.release_created }} 58 | runs-on: ubuntu-latest 59 | permissions: 60 | contents: write 61 | id-token: write 62 | steps: 63 | - name: Download artifact 64 | uses: actions/download-artifact@v4 65 | with: 66 | name: python-package 67 | path: dist/ 68 | 69 | - name: Publish package to PyPI 70 | uses: pypa/gh-action-pypi-publish@release/v1 71 | 72 | - name: Upload Release Assets 73 | uses: softprops/action-gh-release@v2 74 | with: 75 | # 直接从 release-please job 获取 tag_name 76 | tag_name: ${{ needs.release-please.outputs.tag_name }} 77 | files: dist/* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *$py.class 4 | *.pyc 5 | *.pyo 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | debug.log 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .nox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | .target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # IPython 76 | profile_default/ 77 | ipython_config.py 78 | 79 | # pyenv 80 | .python-version 81 | 82 | # pipenv 83 | Pipfile.lock 84 | 85 | # poetry 86 | poetry.lock 87 | 88 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 89 | __pypackages__/ 90 | 91 | # Celery stuff 92 | celerybeat-schedule 93 | celerybeat.pid 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | 121 | # Pyre type checker 122 | .pyre/ 123 | 124 | # pytype static type analyzer 125 | .pytype/ 126 | 127 | # Cython debug symbols 128 | cython_debug/ 129 | 130 | # VS Code settings 131 | .vscode/ 132 | src/omni_bot_sdk/services/core/database_service.py 133 | src/omni_bot_sdk/utils/fuck_zxl.py 134 | src/omni_bot_sdk/yolo/models/msg_rec2.pt 135 | examples/simple-bot/aes_xor_key.dat 136 | examples/simple-bot/config.yaml 137 | aes_xor_key.dat 138 | runtime_images 139 | test_image.png 140 | config.yaml 141 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.0.0](https://github.com/weixin-omni/omni-bot-sdk-oss/compare/v1.0.6...v2.0.0) (2025-08-12) 4 | 5 | 6 | ### ⚠ BREAKING CHANGES 7 | 8 | * 添加支持的版本说明 9 | * 支持的微信最低版本调整,不再兼容老版本,最低支持 4.0.6.33 10 | * 查找图片密钥 11 | 12 | ### feature 13 | 14 | * 查找图片密钥 ([2925db4](https://github.com/weixin-omni/omni-bot-sdk-oss/commit/2925db4819295ea0ac5f87901a244d76d877bb0c)) 15 | * 添加支持的版本说明 ([27bbe29](https://github.com/weixin-omni/omni-bot-sdk-oss/commit/27bbe292aa1ba6a0a36fc5cc085daad3b1d74479)) 16 | 17 | 18 | ### doc 19 | 20 | * 支持的微信最低版本调整,不再兼容老版本,最低支持 4.0.6.33 ([e97088d](https://github.com/weixin-omni/omni-bot-sdk-oss/commit/e97088df7b9a13ab4ad6d38c10a7e216fb14b0d9)) 21 | 22 | ## [1.0.6](https://github.com/weixin-omni/omni-bot-sdk-oss/compare/v1.0.5...v1.0.6) (2025-08-12) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * 查找微信进程方法修改 ([521b330](https://github.com/weixin-omni/omni-bot-sdk-oss/commit/521b330e0243db690824e9671de514d52a71f380)) 28 | 29 | ## [1.0.5](https://github.com/weixin-omni/omni-bot-sdk-oss/compare/v1.0.4...v1.0.5) (2025-08-12) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * 在1080P下初始化数组越界 ([aa8cd49](https://github.com/weixin-omni/omni-bot-sdk-oss/commit/aa8cd494042cbb408636d02103b952610aaf41ea)) 35 | 36 | ## [1.0.4](https://github.com/weixin-omni/omni-bot-sdk-oss/compare/v1.0.3...v1.0.4) (2025-08-11) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * 部分链接解析失败 ([5c98158](https://github.com/weixin-omni/omni-bot-sdk-oss/commit/5c981583ca87044b90a8fff12756e7f548c940dd)) 42 | 43 | ## [1.0.3](https://github.com/weixin-omni/omni-bot-sdk-oss/compare/v1.0.2...v1.0.3) (2025-07-21) 44 | 45 | 46 | ### Bug Fixes 47 | 48 | * fix ci/cd error, which caused package bug ([28cce5c](https://github.com/weixin-omni/omni-bot-sdk-oss/commit/28cce5ca2a7f54341f95de53fb7f9cd7108dda40)) 49 | 50 | ## [1.0.2](https://github.com/weixin-omni/omni-bot-sdk-oss/compare/v1.0.1...v1.0.2) (2025-07-20) 51 | 52 | 53 | ### Bug Fixes 54 | 55 | * 企业微信添加好友,弹窗不同 ([b47538e](https://github.com/weixin-omni/omni-bot-sdk-oss/commit/b47538e4345c7295476e085be83a4d55ac43f653)) 56 | 57 | ## [1.0.1](https://github.com/weixin-omni/omni-bot-sdk-oss/compare/v1.0.0...v1.0.1) (2025-07-20) 58 | 59 | 60 | ### Bug Fixes 61 | 62 | * refactor import new error. ([d6e351e](https://github.com/weixin-omni/omni-bot-sdk-oss/commit/d6e351eef2c4275a85863dedfe4965c171107b93)) 63 | 64 | ## [1.0.0](https://github.com/weixin-omni/omni-bot-sdk-oss/compare/v0.1.0...v1.0.0) (2025-07-20) 65 | 66 | 67 | ### ⚠ BREAKING CHANGES 68 | 69 | * improve security, remove db pool 70 | 71 | ### Bug Fixes 72 | 73 | * fix [#4](https://github.com/weixin-omni/omni-bot-sdk-oss/issues/4) closes [#4](https://github.com/weixin-omni/omni-bot-sdk-oss/issues/4) ([1ba8a04](https://github.com/weixin-omni/omni-bot-sdk-oss/commit/1ba8a04badbbaeb6043b27d58dbeed3e5ef3595e)) 74 | 75 | 76 | ### Documentation 77 | 78 | * Update README.md ([b940871](https://github.com/weixin-omni/omni-bot-sdk-oss/commit/b940871d2b38843cb91572c249b19390cf862f64)) 79 | 80 | 81 | ### Code Refactoring 82 | 83 | * improve security, remove db pool ([79df0fb](https://github.com/weixin-omni/omni-bot-sdk-oss/commit/79df0fbfa238fd7dd1159016696c86d37c15bee6)) 84 | 85 | ## 0.1.0 (2025-07-12) 86 | 87 | 88 | ### Features 89 | 90 | * init project ([91abcc6](https://github.com/weixin-omni/omni-bot-sdk-oss/commit/91abcc603112e0a4c3b6c3db6efe4374da5123eb)) 91 | 92 | 93 | ### Documentation 94 | 95 | * init doc ([a3c46bb](https://github.com/weixin-omni/omni-bot-sdk-oss/commit/a3c46bb94a10cbeb15e8aed41d0fd674b9522d3b)) 96 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # omni-bot-sdk-oss 贡献指南 2 | 3 | 感谢你对 omni-bot-sdk-oss 的关注!在提交贡献前,请阅读本指南,以便更高效地协作。 4 | 5 | ## 项目简介 6 | 7 | omni-bot-sdk-oss 是 omni-bot 项目的开源 SDK,提供了 bot 框架、插件加载、消息分发等核心能力。该仓库主要包含: 8 | 9 | - SDK 核心代码(`src/omni_bot_sdk/`) 10 | - 示例(`examples/`) 11 | - 文档(`docs/`) 12 | 13 | ## 开发环境 14 | 15 | - Python 3.12 16 | - Windows 10/11 17 | - 推荐编辑器:VS Code + Python 扩展 18 | - 依赖管理:pip(或 poetry,如有) 19 | - 代码格式化/检查:ruff、black 20 | - 单元测试:pytest 21 | 22 | ## 快速开始 23 | 24 | 1. 克隆仓库并进入目录 25 | 26 | ```bash 27 | git clone git@github.com:weixin-omni/omni-bot-sdk-oss.git 28 | cd omni-bot-sdk-oss 29 | ``` 30 | 31 | 2. 建议使用虚拟环境 32 | 33 | ```bash 34 | python -m venv .venv 35 | # Windows: 36 | .venv\Scripts\activate 37 | ``` 38 | 39 | 3. 安装依赖 40 | 41 | ```bash 42 | pip install -e . 43 | ``` 44 | 45 | 4. 运行示例 46 | 47 | ```bash 48 | cd examples/simple-bot 49 | python bot.py 50 | ``` 51 | 52 | ## 代码结构 53 | 54 | ```text 55 | src/omni_bot_sdk/ # SDK 主体 56 | ├── bot.py # Bot 主类 57 | ├── plugins/ # 插件相关 58 | ├── clients/ # 外部服务客户端 59 | ├── rpa/ # RPA 相关 60 | ├── mcp/ # 消息分发/协议 61 | ├── utils/ # 工具函数 62 | └── ... # 其他模块 63 | examples/ # 示例代码 64 | docs/ # 文档 65 | tests/ # 测试用例(如有) 66 | ``` 67 | 68 | ## 贡献流程 69 | 70 | 1. Fork 仓库,创建新分支 71 | 2. 保持代码风格一致,建议提交前运行 72 | 73 | ```bash 74 | black . 75 | ``` 76 | 77 | 3. 保证测试通过 78 | 79 | ```bash 80 | pytest 81 | ``` 82 | 83 | 4. 提交 PR,建议标题格式为 `: `,如 `feat: 新增插件管理功能` 84 | 5. 如需文档更新,请同步修改 `docs/` 目录 85 | 86 | ## 插件开发建议 87 | 88 | - 插件需实现 `src/omni_bot_sdk/plugins/interface.py` 中定义的接口 89 | - 插件注册与加载请参考 `plugin_manager.py` 90 | - 示例插件可参考 `examples/simple-bot/` 91 | 92 | ## 版本发布(维护者) 93 | 94 | - 版本号请同步修改 `pyproject.toml`、`src/omni_bot_sdk/__init__.py` 等相关文件 95 | - 构建与发布请参考 `MANIFEST.in` 和相关脚本 96 | 97 | ## 其他说明 98 | 99 | - 建议所有新代码添加类型注解和必要注释 100 | - 如有疑问,请在 issue 区留言,或参与讨论 101 | 102 | --- 103 | 104 | 因为有你,omni-bot-sdk-oss 会变得更好,感谢你的贡献! 105 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | global-include *.py 2 | recursive-include src/omni_bot_sdk/yolo/models *.pt 3 | recursive-include src/omni_bot_sdk *.pyd 4 | include config.example.yaml 5 | exclude src/omni_bot_sdk/utils/fuck_zxl.py 6 | exclude src/omni_bot_sdk/services/core/database_service.py 7 | exclude src/omni_bot_sdk/utils/fuck_zxl.pyi 8 | exclude src/omni_bot_sdk/services/core/database_service.pyi 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # omni-bot-sdk 2 | 3 | > ⚠️ **使用风险警告** 4 | > 5 | > 使用本项目存在微信账号被封禁、功能受限等风险,**项目及作者概不承担任何责任**。 6 | > 7 | > **🫸强烈建议不要使用任何在线 LLM API(如 OpenAI、百度、讯飞等)以免造成个人信息泄漏。** 8 | > 9 | > 在使用过程中,请务必遵守微信的各项行为准则,避免违规操作,降低封号概率。 10 | > 11 | > --- 12 | > 13 | > **🙅‍♂️禁止使用范围:** 14 | > 15 | > 本项目**严禁用于营销、发广告等任何企业或商业行为**,仅推荐用于个人学习和技术交流用途。 16 | > 17 | > **☂️隐私与安全声明:** 18 | > 19 | > 本项目**不联网,不收集任何用户数据**,所有运行数据均保留在用户电脑本地。 20 | > 21 | > 本项目**不会对微信数据库进行任何写操作**,不影响微信的正常运行。 22 | 23 |

24 | 25 | PyPI version 26 | 27 | 28 | License 29 | 30 | 31 | Python Version 32 | 33 |

34 | 35 |

36 | 支持微信版本: 4.0.6.33 37 |

38 | 39 | > 🤖👁️🖥️ 一个基于视觉识别,运行时零侵入的微信4.0 RPA 框架,构建个微聊天机器人,快速接入LLM,Dify 40 | 41 |

42 | 项目演示 43 |

44 | 45 | ## ✨ 特性 46 | 47 | * **接收消息零延迟**:基于数据库的监听策略,几乎零延迟接收消息 48 | * **运行时零侵入**:采用特殊训练的YOLO模型 + OCR识别定位策略,对微信程序零HOOK,降低被检测概率 49 | * **MCP Tool**: 支持mcp调用rpa,和微信进行交互,发送消息,群管理操作等 50 | * **RPA可扩展**:可轻松自定义RPA扩展,支持更多的动作,包括 发送朋友圈,发送小程序等 51 | * **插件化架构**:通过插件系统轻松扩展你的机器人功能,保持主逻辑清晰。 52 | * **理论支持最新版本微信**:基于视觉识别的RPA,几乎不受微信版本更新影响 53 | * **快速启动**:几行代码,即可启动一个RPA机器人 54 | * **异常自恢复**: 微信异常的情况下尝试自动回复RPA,失败自动推送钉钉登录二维码 55 | 56 | ## 🚀 快速开始 57 | 58 | 注意,基于RPA方案,在运行时,请勿操作鼠标键盘,以免影响RPA运行 59 | 60 | 61 | ### 1. 安装 62 | 63 | **务必使用Python3.12** 64 | 65 | 通过 pip 从 PyPI 安装: 66 | 67 | ```bash 68 | pip install omni-bot-sdk 69 | ``` 70 | 71 | ### 2. 获取数据库密钥 72 | 73 | **本项目不提供此工具,可自行通过github获取 DbkeyHookCMD.exe或者DbkeyHookUI.exe进行获取 74 | 获取数据库密钥后,填入配置文件中的 dbkey,请查阅鸣谢章节获取** 75 | 76 | ### 3. 启动MQTT服务 77 | 78 | mqtt服务用于MCP消息转发,以及后续更新任务执行结果回调,windows可以使用nanomq直接启动本地服务 79 | 80 | ### 4. "Hello, World" 81 | 82 | **🔎🔑注意:重新启动一次微信,并登录,然后立即启动RPA,否则可能无法获取图片解密密钥,微信会在启动后一段时间污染密钥,RPA启动后会自动给文件传输助手发送一张图片,然后执行获取密钥** 83 | 84 | 参考 config.example,生成 config.yaml ,然后几行代码即可启动RPA 85 | 86 | ```python 87 | from omni_bot_sdk.bot import Bot 88 | 89 | def main(): 90 | bot = Bot(config_path="config.yaml") 91 | bot.start() 92 | 93 | if __name__ == "__main__": 94 | main() 95 | ``` 96 | 97 | 现在,去和你的机器人聊天吧! 98 | 99 | ## 🖥️ 桌面版 GUI 客户端 100 | 101 | 如果您喜欢直接使用桌面版应用程序,我们为您准备了一个可视化管理客户端,无需编写代码即可轻松上手。 102 | 103 | 👉 [前往 omni-bot-gui 仓库](https://github.com/weixin-omni/omni-bot-gui) 104 | 105 | ## 项目架构 106 | 107 | ```mermaid 108 | graph LR 109 | subgraph "消息源 (Source)" 110 | direction LR 111 | DB[(数据库)] 112 | end 113 | 114 | subgraph "核心处理框架 (Core Framework)" 115 | direction TB 116 | Poller{轮询器} -->|发现新消息| MsgQueue([消息队列]) 117 | MsgQueue --> Consumer[消息消费者/解析器] 118 | Consumer --> PluginManager[/插件管理器/] 119 | 120 | subgraph "插件链 (Plugin Chain)" 121 | PluginManager -->|输入消息| Plugin1 122 | Plugin1 --> Plugin2 123 | Plugin2 --> ... 124 | end 125 | 126 | ... -->|输出Action| PluginManager 127 | PluginManager --> |汇总动作清单| RPAQueue([RPA动作队列]) 128 | end 129 | 130 | subgraph "RPA执行端 (Executor)" 131 | direction TB 132 | RPA_Consumer[RPA消费者] --> RPA_Handler(Action Handler) 133 | RPA_Handler --> WeChat((微信交互)) 134 | end 135 | 136 | DB -- 定时读取 --> Poller 137 | RPAQueue --> RPA_Consumer 138 | ``` 139 | 140 | ## 📚 插件文档 141 | 142 | 想要了解更多高级用法和完整的 API 参考吗? 143 | 144 | 👉 **请查阅我们的 [插件仓库](https://github.com/weixin-omni/omni-bot-plugins-oss)** 👈 145 | 146 | 内置了两个插件 147 | 148 | * 自己发送的消息阻断,不再触发后续的插件处理 149 | * 没有群名称的消息阻断,目前无法处理这类消息 150 | 151 | ## 🧩 示例 152 | 153 | 我们提供了一个包含丰富示例的目录,帮助你快速实现各种功能。 154 | 155 | 你可以在本仓库的 `examples/` 目录下找到所有示例代码。 156 | 157 | ## 🤝 贡献 158 | 159 | 我们热烈欢迎任何形式的贡献!无论是提交 Issue、修复 Bug 还是添加新功能。 160 | 161 | 在开始之前,请先阅读我们的 **[贡献指南 (CONTRIBUTING.md)](CONTRIBUTING.md)**。 162 | 163 | 开发环境设置: 164 | 165 | ```bash 166 | # 1. 克隆仓库 167 | git clone https://github.com/weixin-omni/omni-bot-sdk-oss 168 | cd omni-bot-sdk-oss 169 | 170 | # 2. 创建并激活虚拟环境 (推荐) 171 | python -m venv venv 172 | source venv/bin/activate # on Windows: venv\Scripts\activate 173 | 174 | # 3. 安装开发依赖 175 | pip install -e . 176 | 177 | ``` 178 | 179 | ## 💀 项目局限性 180 | 181 | * RPA操作基于文字识别和YOLO识别,无法保证100%准确以及操作的100%成功,鲁棒性不如接口逆向或Hook的框架 182 | * 消息发送需要指定联系人,采用和人类操作完全相同的策略,当出现同名的联系人或者群聊时,无法准确找到目标对话,导致操作失败或错误 183 | * RPA操作不能打断,由于RPA操作必须是线性的,因此RPA框架运行时,会和用户抢夺鼠标键盘的操控权,最佳情况是在单独的机器上进行部署 184 | * 窗口不能准确获取,虽然已经采取了一部分措施来防止获取到错误的窗口,但是目前不能保证100%,如果有同名的窗口标题,可能导致错误的窗口焦点 185 | * 初始化流程长,由于RPA初始化需要对不同的控件进行定位,因此需要花费一点时间,这是正常的。完全采用视觉定位,无法避免。 186 | * 数据获取方案可被检测,目前采用数据库轮询的方案获取最新消息,使用只读模式打开微信的db文件,会出现文件句柄,具体表现和杀毒软件扫描文件应该是类似的。后续会优化,在不读取的时候释放掉句柄。 187 | 188 | ## ❓FAQ 189 | 190 | 可以加Omni-bot的开发者交流群,请注明omni-bot,机器人会自动通过,每天自动通过人数有限,请耐心等待 191 | 192 |

193 | 交流群 194 | 群主bot 195 |

196 | 197 | (如果项目对你有用,也可以请我喝杯咖啡 ☕️ ~) 198 | 199 |

200 | 赞赏码 201 |

202 | 203 | ## 🗺️ 路线图 (Roadmap) 204 | 205 | 我们对项目的未来有一些规划,欢迎你加入讨论或贡献代码! 206 | 207 | * [ ] 更换图片解析的方式,支持WXGF解析 [图片解析项目](https://github.com/recarto404/WxDatDecrypt) 208 | * [ ] 优化消息获取机制,增强反屏蔽能力 209 | * [ ] 增强鲁棒性,对于视觉识别异常调用VL模型进一步处理 210 | * [ ] 增加更多免费插件 211 | * [ ] 一个更加完善的GUI客户端(目前有一个简化版) 212 | * [ ] 编写测试用例(嘿嘿嘿嘿嘿,这一条就先看看) 213 | 214 | ## 📄 许可证 215 | 216 | 本项目基于 [GPL-V3](LICENSE) 许可证开源。 217 | 218 | ## ❤️ 致谢与引用 219 | 220 | 本项目在开发过程中参考和使用了以下开源项目的代码和技术,在此向这些项目的作者和贡献者表示衷心的感谢: 221 | 222 | ### 微信数据库解析相关 223 | 224 | - **[DbkeyHook](https://github.com/gzygood/DbkeyHook)** - 微信数据库密钥获取工具,是本项目运行的前提条件 225 | * **[wechat-dump-rs](https://github.com/0xlane/wechat-dump-rs)** - 基于Rust的微信数据库解析工具,为本项目提供了数据库读取的技术参考 226 | * **[WeChatMsg](https://github.com/LC044/WeChatMsg)** - 微信聊天记录导出工具,提供了微信消息格式解析的重要参考 227 | * **[wechat-dump](https://github.com/ppwwyyxx/wechat-dump)** - 微信数据库导出工具,WXGF格式的解析参考 228 | 229 | ### 微信文件解析相关 230 | 231 | - **[WxDatDecrypt](https://github.com/recarto404/WxDatDecrypt)** - 微信文件解密工具,为图片和媒体文件的解析提供了技术支持 232 | 233 | 这些优秀的开源项目为omni-bot-sdk的开发奠定了重要的技术基础。我们感谢这些项目的开源精神,并承诺在遵循各自许可证的前提下使用这些技术。 234 | 235 | ## ⚠️ 免责声明 236 | 237 | 本项目仅供学习和技术交流使用,严禁用于任何商业用途或违反相关法律法规的行为。因使用本项目产生的任何后果,均由用户自行承担,项目作者不承担任何法律责任。 238 | 239 | ## 🌟 Star History 240 | 241 | 242 | 243 | 244 | 245 | Star History Chart 246 | 247 | 248 | -------------------------------------------------------------------------------- /config.example.yaml: -------------------------------------------------------------------------------- 1 | dbkey: # 数据库加密密钥,必须设置 2 | 3 | # AES加密用的密钥和XOR,格式为字符串, 设置为空,自动查找 4 | # 示例:aes_xor_key: 1234567890,17 5 | aes_xor_key: 6 | 7 | # MCP服务配置 8 | mcp: 9 | # 监听主机地址,通常为0.0.0.0表示所有网卡 10 | host: 0.0.0.0 11 | # 监听端口 12 | port: 8000 13 | 14 | dingtalk: 15 | # 钉钉机器人Webhook地址,当微信异常时,将推送异常消息和登录二维码 16 | webhook_url: https://example.com/robot/send?access_token=YOUR_DINGTALK_TOKEN 17 | 18 | logging: 19 | # 日志文件最多保留数量 20 | backup_count: 5 21 | # 日志级别,可选:DEBUG/INFO/WARNING/ERROR 22 | level: INFO 23 | # 单个日志文件最大字节数 24 | max_size: 10485760 25 | # 日志文件存放路径 26 | path: logs 27 | 28 | mqtt: 29 | # MQTT客户端ID前缀 30 | client_id: weixin_omni 31 | # MQTT服务器地址 32 | host: 127.0.0.1 33 | # MQTT密码 34 | password: 'YOUR_MQTT_PASSWORD' 35 | # MQTT端口 36 | port: 1883 37 | # MQTT用户名 38 | username: weixin 39 | 40 | plugins: 41 | # 图片密钥辅助插件 42 | image-aes-plugin: 43 | enabled: true 44 | # 空群屏蔽插件 45 | block-empty-room-plugin: 46 | enabled: true 47 | priority: 2000 48 | # 聊天上下文插件 49 | chat-context-plugin: 50 | enabled: true 51 | priority: 1999 52 | # OpenAI对话插件 53 | openai-bot-plugin: 54 | enabled: true 55 | priority: 1497 56 | openai_api_key: YOUR_OPENAI_API_KEY 57 | openai_base_url: http://example.com:7860 58 | openai_model: gemini-2.0-flash # 使用的模型名 59 | prompt: 你是一个聊天机器人,请根据用户的问题给出回答。历史对话:{{chat_history}} 当前时间:{{time_now}} 60 | 你的昵称:{{self_nickname}} 群昵称:{{room_nickname}} 消息来自于:{{contact_nickname}} 61 | # 自发消息插件 62 | self-msg-plugin: 63 | enabled: true 64 | priority: 1998 65 | 66 | rpa: 67 | # RPA相关参数 68 | action_delay: 0.3 # 操作延迟(秒) 69 | scroll_delay: 1 # 滚动延迟(秒) 70 | max_retries: 3 # 最大重试次数 71 | switch_contact_delay: 0.3 # 切换联系人延迟(秒) 72 | ocr: 73 | merge_threshold: 5.0 # OCR合并阈值 74 | min_confidence: 0.5 # 最小置信度 75 | remote_url: http://example.com/ocr # 远程OCR服务地址 76 | use_remote: false # 是否使用远程OCR 77 | room_action_offset: 78 | - 0 79 | - -30 80 | search_contact_offset: 81 | - 0 82 | - 40 83 | side_bar_delay: 3 84 | timeout: 30 85 | window_margin: 20 86 | window_show_delay: 1.5 87 | short_term_rate: 0.15 88 | short_term_capacity: 2 89 | long_term_rate: 0.25 90 | long_term_capacity: 10 91 | 92 | wxgf: 93 | # 微信GF解析API地址,暂时未开放,查看roammap更新计划 94 | api_url: http://example.com/api/v1/decrypt 95 | 96 | s3: 97 | # S3存储配置,微信登录二维码将会推送到这里,建议使用CF的R2免费存储,并设置过期时间1天 98 | endpoint_url: https://example.com 99 | access_key: YOUR_S3_ACCESS_KEY 100 | secret_key: YOUR_S3_SECRET_KEY 101 | region: apac 102 | bucket: weixin-login-qr 103 | public_url_prefix: https://example.com/ 104 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .temp 3 | .cache 4 | dist/ 5 | lib/ 6 | *.tsbuildinfo 7 | .DS_Store 8 | .vitepress/cache 9 | -------------------------------------------------------------------------------- /docs/.prettierignore: -------------------------------------------------------------------------------- 1 | *.html 2 | dist/ 3 | node_modules/ 4 | *.min.js 5 | lib/* 6 | pnpm-lock.yaml 7 | -------------------------------------------------------------------------------- /docs/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "es5", 6 | "semi": false, 7 | "arrowParens": "always", 8 | "overrides": [ 9 | { 10 | "files": "*.md", 11 | "options": { 12 | "tabWidth": 3 13 | } 14 | }, 15 | { 16 | "files": "*.json5", 17 | "options": { 18 | "singleQuote": false 19 | } 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /docs/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | import { 3 | groupIconMdPlugin, 4 | groupIconVitePlugin, 5 | localIconLoader, 6 | } from 'vitepress-plugin-group-icons' 7 | import llmstxt from 'vitepress-plugin-llms' 8 | import { withMermaid } from "vitepress-plugin-mermaid"; 9 | 10 | export default withMermaid({ 11 | lang: 'zh-CN', 12 | title: 'omni-bot', 13 | description: '🤖 一个基于视觉识别的微信4.0 RPA框架', 14 | cleanUrls: true, 15 | head: [ 16 | ['link', { rel: 'icon', href: '/favicon.ico' }], 17 | ['meta', { name: 'theme-color', content: '#67e8e2' }], 18 | ['meta', { property: 'og:type', content: 'website' }], 19 | ['meta', { property: 'og:locale', content: 'zh-CN' }], 20 | ['meta', { property: 'og:title', content: '🤖 一个基于视觉识别的微信4.0 RPA框架' }], 21 | ['meta', { property: 'og:site_name', content: 'omni-bot' }], 22 | /* ['meta', { property: 'og:image', content: 'https://yutto.nyakku.moe/logo.png' }], 23 | ['meta', { property: 'og:url', content: 'https://yutto.nyakku.moe/' }], */ 24 | ], 25 | themeConfig: { 26 | logo: { src: '/logo.png', width: 24, height: 24 }, 27 | nav: [ 28 | { text: '首页', link: '/' }, 29 | { text: '指南', link: '/guide/quick-start' }, 30 | { 31 | text: '支持我', 32 | items: [ 33 | { text: '赞助', link: '/sponsor' }, 34 | { 35 | text: '参与贡献', 36 | link: 'https://github.com/weixin-omni/omni-bot-sdk-oss/blob/master/CONTRIBUTING.md', 37 | }, 38 | ], 39 | }, 40 | ], 41 | 42 | sidebar: { 43 | '/guide': [ 44 | { 45 | text: '快速开始', 46 | link: '/guide/quick-start', 47 | }, 48 | { 49 | text: '基础概念', 50 | link: '/guide/concepts', 51 | }, 52 | { 53 | text: '配置指南', 54 | link: '/guide/configuration', 55 | }, 56 | { 57 | text: '消息处理', 58 | link: '/guide/message-handling', 59 | }, 60 | { 61 | text: '插件开发', 62 | link: '/guide/plugins', 63 | }, 64 | { 65 | text: 'Dify 接入', 66 | link: '/guide/dify-integration', 67 | }, 68 | { 69 | text: 'RPA操作', 70 | link: '/guide/rpa-operations', 71 | }, 72 | { 73 | text: 'MCP集成', 74 | link: '/guide/mcp-integration', 75 | }, 76 | { 77 | text: '部署指南', 78 | link: '/guide/deployment', 79 | }, 80 | { 81 | text: '故障排除', 82 | link: '/guide/troubleshooting', 83 | }, 84 | { 85 | text: 'FAQ', 86 | link: '/guide/faq', 87 | }, 88 | ], 89 | }, 90 | 91 | footer: { 92 | message: 'Released under the GPL3.0 License.', 93 | copyright: 'Copyright © 2025-present weixin-omni', 94 | }, 95 | 96 | editLink: { 97 | pattern: 'https://github.com/weixin-omni/omni-bot-sdk-oss/edit/master/docs/:path', 98 | text: '修订此文档', 99 | }, 100 | 101 | socialLinks: [ 102 | { icon: 'github', link: 'https://github.com/weixin-omni/omni-bot-sdk-oss' }, 103 | ], 104 | 105 | search: { 106 | provider: 'local', 107 | }, 108 | }, 109 | mermaid:{ 110 | //mermaidConfig !theme here works for ligth mode since dark theme is forced in dark mode 111 | }, 112 | markdown: { 113 | image: { 114 | lazyLoading: true, 115 | }, 116 | config(md) { 117 | md.use(groupIconMdPlugin) 118 | }, 119 | }, 120 | vite: { 121 | plugins: [ 122 | llmstxt(), 123 | groupIconVitePlugin({ 124 | customIcon: { 125 | yutto: localIconLoader(import.meta.url, '../public/favicon.ico'), 126 | }, 127 | }) as any, 128 | ], 129 | }, 130 | }) 131 | -------------------------------------------------------------------------------- /docs/.vitepress/env.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue' 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/Layout.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/Contributors.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 43 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/GitHubUser.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | 19 | 68 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/Sparkler.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 29 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Colors 3 | * -------------------------------------------------------------------------- */ 4 | 5 | :root { 6 | --vp-c-brand-1: #379c9c; 7 | --vp-c-brand-2: #46bcc0; 8 | --vp-c-brand-3: #67e8e2; 9 | --vp-c-brand-soft: #29c9cf33; 10 | } 11 | 12 | .dark { 13 | --vp-c-brand-1: #32cbcb; 14 | --vp-c-brand-2: #3dd6db; 15 | --vp-c-brand-3: #67e8e2; 16 | --vp-c-brand-soft: #29c9cf33; 17 | } 18 | 19 | /** 20 | * Component: Button 21 | * -------------------------------------------------------------------------- */ 22 | 23 | :root { 24 | --vp-button-brand-text: var(--vp-c-bg-soft); 25 | --vp-button-brand-bg: var(--vp-c-brand-2); 26 | --vp-button-brand-hover-text: var(--vp-c-bg-soft); 27 | --vp-button-brand-hover-bg: var(--vp-c-brand-3); 28 | --vp-button-brand-active-text: var(--vp-c-bg-soft); 29 | --vp-button-brand-active-bg: var(--vp-c-brand-2); 30 | } 31 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { EnhanceAppContext } from 'vitepress' 2 | import DefaultTheme from 'vitepress/theme' 3 | import Layout from './Layout.vue' 4 | import 'virtual:group-icons.css' 5 | import './index.css' 6 | 7 | export default { 8 | ...DefaultTheme, 9 | Layout, 10 | enhanceApp(ctx: EnhanceAppContext) { 11 | // extend default theme custom behaviour. 12 | DefaultTheme.enhanceApp(ctx) 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /docs/env.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue' 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /docs/guide/concepts.md: -------------------------------------------------------------------------------- 1 | # 基础概念 2 | 3 | ## RPA 4 | 基于视觉识别的微信自动化操作,采用YOLO模型+OCR定位控件,实现零HOOK。 5 | 6 | ## 插件架构 7 | 通过插件系统扩展机器人功能,保持主逻辑清晰。 8 | 9 | ## MCP Tool 10 | 支持mcp调用rpa,和微信进行交互,发送消息,群管理操作等。 11 | 12 | ## 消息流转 13 | - 数据库监听新消息 14 | - 插件链处理消息 15 | - RPA队列执行动作 16 | 17 | ## 架构图 18 | 19 | ```mermaid 20 | graph LR 21 | subgraph "消息源 (Source)" 22 | direction LR 23 | DB[(数据库)] 24 | end 25 | subgraph "核心处理框架 (Core Framework)" 26 | direction TB 27 | Poller{轮询器} -->|发现新消息| MsgQueue([消息队列]) 28 | MsgQueue --> Consumer[消息消费者/解析器] 29 | Consumer --> PluginManager[/插件管理器/] 30 | subgraph "插件链 (Plugin Chain)" 31 | PluginManager -->|输入消息| Plugin1 32 | Plugin1 --> Plugin2 33 | Plugin2 --> ... 34 | end 35 | ... -->|输出Action| PluginManager 36 | PluginManager --> |汇总动作清单| RPAQueue([RPA动作队列]) 37 | end 38 | subgraph "RPA执行端 (Executor)" 39 | direction TB 40 | RPA_Consumer[RPA消费者] --> RPA_Handler(Action Handler) 41 | RPA_Handler --> WeChat((微信交互)) 42 | end 43 | DB -- 定时读取 --> Poller 44 | RPAQueue --> RPA_Consumer 45 | ``` -------------------------------------------------------------------------------- /docs/guide/configuration.md: -------------------------------------------------------------------------------- 1 | # 配置指南 2 | 3 | 本章节详细说明所有可用的配置项及其作用,无需再查阅配置文件。 4 | 5 | --- 6 | 7 | ## 顶层配置 8 | 9 | ### dbkey 10 | - **说明**:数据库加密密钥,必须设置。 11 | - **类型**:字符串 12 | 13 | ### aes_xor_key 14 | - **说明**:AES加密用的密钥和XOR,格式为字符串,设置为空时自动查找。 15 | - **示例**:`1234567890,17` 16 | 17 | --- 18 | 19 | ## MCP 服务配置(mcp) 20 | - **host**:监听主机地址,通常为`0.0.0.0`表示所有网卡。 21 | - **port**:监听端口,默认`8000`。 22 | 23 | --- 24 | 25 | ## 钉钉机器人(dingtalk) 26 | - **webhook_url**:钉钉机器人Webhook地址,当微信异常时,将推送异常消息和登录二维码。 27 | 28 | --- 29 | 30 | ## 日志配置(logging) 31 | - **backup_count**:日志文件最多保留数量。 32 | - **level**:日志级别,可选:`DEBUG`/`INFO`/`WARNING`/`ERROR`。 33 | - **max_size**:单个日志文件最大字节数,默认`10485760`(10MB)。 34 | - **path**:日志文件存放路径。 35 | 36 | --- 37 | 38 | ## MQTT 配置(mqtt) 39 | - **client_id**:MQTT客户端ID前缀。 40 | - **host**:MQTT服务器地址。 41 | - **password**:MQTT密码。 42 | - **port**:MQTT端口,默认`1883`。 43 | - **username**:MQTT用户名。 44 | 45 | --- 46 | 47 | ## 插件配置(plugins) 48 | - **block-empty-room-plugin**:空群屏蔽插件,`enabled`(是否启用),`priority`(优先级)。 49 | - **chat-context-plugin**:聊天上下文插件,`enabled`,`priority`。 50 | - **openai-bot-plugin**:OpenAI对话插件,`enabled`,`priority`,`openai_api_key`,`openai_base_url`,`openai_model`(模型名),`prompt`(对话提示词模板)。 51 | - **self-msg-plugin**:自发消息插件,`enabled`,`priority`。 52 | 53 | --- 54 | 55 | ## RPA 相关参数(rpa) 56 | - **action_delay**:操作延迟(秒)。 57 | - **scroll_delay**:滚动延迟(秒)。 58 | - **max_retries**:最大重试次数。 59 | - **switch_contact_delay**:切换联系人延迟(秒)。 60 | - **ocr**:OCR相关配置: 61 | - `merge_threshold`:OCR合并阈值。 62 | - `min_confidence`:最小置信度。 63 | - `remote_url`:远程OCR服务地址。 64 | - `use_remote`:是否使用远程OCR。 65 | - **room_action_offset**:房间操作偏移量,数组。 66 | - **search_contact_offset**:搜索联系人偏移量,数组。 67 | - **side_bar_delay**:侧边栏延迟(秒)。 68 | - **timeout**:超时时间(秒)。 69 | - **window_margin**:窗口边距。 70 | - **window_show_delay**:窗口显示延迟(秒)。 71 | - **short_term_rate**:短期速率。 72 | - **short_term_capacity**:短期容量。 73 | - **long_term_rate**:长期速率。 74 | - **long_term_capacity**:长期容量。 75 | 76 | --- 77 | 78 | ## 微信GF解析(wxgf) 79 | - **api_url**:微信GF解析API地址。 80 | 81 | --- 82 | 83 | ## S3 存储配置(s3) 84 | - **endpoint_url**:S3服务端点。 85 | - **access_key**:S3访问密钥。 86 | - **secret_key**:S3密钥。 87 | - **region**:S3区域。 88 | - **bucket**:桶名称。 89 | - **public_url_prefix**:公开访问前缀。 90 | 91 | --- 92 | 93 | 如需详细配置示例,请参考 `config.example.yaml` 文件。 -------------------------------------------------------------------------------- /docs/guide/deployment.md: -------------------------------------------------------------------------------- 1 | # 部署指南 2 | 3 | ## 开发环境设置 4 | 5 | ```bash 6 | # 1. 克隆仓库 7 | git clone https://github.com/weixin-omni/omni-bot-sdk-oss 8 | cd omni-bot-sdk-oss 9 | 10 | # 2. 创建并激活虚拟环境 (推荐) 11 | python -m venv venv 12 | source venv/bin/activate # on Windows: venv\Scripts\activate 13 | 14 | # 3. 安装开发依赖 15 | pip install -e . 16 | ``` 17 | 18 | ## 部署建议 19 | - 最佳部署方式为在独立机器上运行,避免鼠标键盘被抢占 20 | - 请勿用于商业用途,遵守相关法律法规 -------------------------------------------------------------------------------- /docs/guide/dify-integration.md: -------------------------------------------------------------------------------- 1 | # Dify 接入指南 2 | 3 | > ⚠️ Dify 插件为高级版功能,用户需自行开发,Omni Bot SDK 已提供底层 Dify API 支持。如需直接使用官方插件,可联系作者获取闭源插件。 4 | 5 | Dify 是一个强大的 LLM 应用开发平台,支持多种大模型和对话能力。Omni Bot SDK 支持通过插件方式集成 Dify,实现智能对话、知识库问答等功能。 6 | 7 | ## 1. 启用 Dify 插件 8 | 9 | 在 `config.yaml` 的 `plugins` 配置中,启用 `dify-bot-plugin`,并填写 Dify 的 API Key 及相关参数: 10 | 11 | ```yaml 12 | plugins: 13 | dify-bot-plugin: 14 | enabled: true 15 | priority: 100 16 | dify_api_key: "你的 Dify API Key" 17 | dify_base_url: "https://api.dify.ai/v1" 18 | conversation_ttl: 180 # 会话过期时间(秒) 19 | ``` 20 | 21 | - `dify_api_key`:在 Dify 控制台获取的 API Key。 22 | - `dify_base_url`:Dify 的 API 地址,通常为 `https://api.dify.ai/v1`。 23 | - `conversation_ttl`:会话过期时间,单位为秒。 24 | - `priority`:插件优先级,数值越大越优先。 25 | 26 | ## 2. 最简插件用法示例 27 | 28 | Dify 插件会自动拦截文本消息,并调用 Dify API 进行智能回复。你只需在配置文件中正确填写参数并启用插件,无需手动调用。 29 | 30 | 如需自定义调用,可在自定义插件中这样使用: 31 | 32 | ```python 33 | from omni_bot_sdk.plugins.interface import Plugin, PluginExcuteContext, SendTextMessageAction 34 | 35 | class MyDifyDemoPlugin(Plugin): 36 | async def handle_message(self, context: PluginExcuteContext): 37 | # 获取用户消息内容 38 | message = context.get_message() 39 | # 伪代码:调用 Dify API 获取回复 40 | ai_reply = "这里是 Dify 返回的智能回复" 41 | # 回复用户 42 | context.add_response( 43 | # 这里只是演示,实际建议直接用官方 dify-bot-plugin 44 | SendTextMessageAction( 45 | content=ai_reply, 46 | target=message.contact.display_name, 47 | is_chatroom=message.is_chatroom, 48 | ) 49 | ) 50 | ``` 51 | 52 | > 推荐直接使用官方 `dify-bot-plugin`,无需重复造轮子。 53 | 54 | ## 3. 获取 Dify API Key 55 | 56 | 1. 注册并登录 [Dify 官网](https://dify.ai/) 57 | 2. 进入「API 密钥」页面,创建并复制你的 API Key 58 | 3. 将 API Key 填入 `config.yaml` 的 `dify_api_key` 字段 59 | 60 | ## 4. 常见问题 61 | 62 | - **Q: Dify 返回 401/403 错误?** 63 | - 检查 API Key 是否正确,是否有调用权限。 64 | - **Q: 如何切换模型?** 65 | - 在 Dify 后台切换模型,无需在插件中配置。 66 | - **Q: 支持知识库问答吗?** 67 | - 支持,需在 Dify 后台配置知识库。 68 | 69 | ## 5. 参考链接 70 | 71 | - [Dify 官方文档](https://docs.dify.ai/zh/) 72 | - [Omni Bot SDK 插件开发](./plugins.md) 73 | 74 | 如有更多 Dify 集成问题,欢迎在社区或 Issue 区反馈。 -------------------------------------------------------------------------------- /docs/guide/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## 常见问题 4 | 5 | - 可以加Omni-bot的开发者交流群,请注明omni-bot,机器人会自动通过,每天自动通过人数有限,请耐心等待 6 | 7 |
8 | 交流群 9 | 作者 10 |
11 | 12 | - 项目仅供学习和技术交流使用,严禁用于任何商业用途 13 | - 本项目不会联网,不收集任何用户数据 14 | - 运行时请勿操作鼠标键盘,以免影响RPA 15 | 16 | 如有更多问题,欢迎提交 Issue 或加入交流群讨论。 -------------------------------------------------------------------------------- /docs/guide/mcp-integration.md: -------------------------------------------------------------------------------- 1 | # MCP集成 2 | 3 | > * 带星号的功能为闭源版本功能,用户需自行开发,开源版本不包含相关代码。 4 | 5 | MCP(Multi-Channel Protocol)为机器人提供了丰富的远程控制和自动化能力,支持消息、群管理、朋友圈等多种操作。以下为当前支持的所有 tool 列表: 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | 47 | 48 | 52 | 53 | 54 | 55 | 56 | 60 | 61 | 62 | 63 | 64 | 68 | 69 | 70 | 71 | 72 | 75 | 76 | 77 | 78 | 79 | 83 | 84 | 85 | 86 | 87 | 91 | 92 | 93 | 94 | 95 | 100 | 101 | 102 | 103 | 104 | 108 | 109 | 110 | 111 | 112 | 116 | 117 | 118 | 119 | 120 | 124 | 125 | 126 | 127 | 128 | 131 | 132 | 133 | 134 |
工具名参数功能说明
get_timestamp获取当前时间的 Unix 时间戳(毫秒)
get_wechat_user_info获取当前登录的微信用户信息(敏感信息已脱敏)
query_wechat_msg 29 | contact_name(对象名称,必填)
30 | query(关键字,选填)
31 | start_timestamp(起始时间,选填)
32 | end_timestamp(结束时间,选填)
33 | limit(最大条数,默认500) 34 |
查询指定用户或群组的微信消息记录,支持关键字、时间范围、数量限制
send_text_msg 40 | recipient_name(对象名称,必填)
41 | message(消息内容,必填)
42 | at_user_name(@群成员,仅群聊,选填) 43 |
向用户或群组发送文本消息,支持@群成员
send_pat_msg 49 | user_name(被拍用户昵称,必填)
50 | room_name(群聊名称,选填) 51 |
发送“拍一拍”消息,支持群聊和单聊
send_file_msg 57 | recipient_name(对象名称,必填)
58 | file_path(本地文件路径,必填) 59 |
向用户或群组发送文件(如图片、视频)
send_pyq* 65 | content(文案,选填)
66 | images(图片路径列表,选填) 67 |
发布一条朋友圈
query_room_member_list 73 | room_name(群聊名称,必填) 74 | 查询指定群聊的成员列表,返回成员的JSON信息
remove_room_member 80 | room_name(群聊名称,必填)
81 | member_name(成员昵称,必填) 82 |
从群聊中移除一个成员
invite_room_member 88 | room_name(群聊名称,必填)
89 | user_name(用户昵称,必填) 90 |
邀请一个用户加入群聊
public_room_announcement 96 | room_name(群聊名称,必填)
97 | content(公告内容,必填)
98 | force_edit(是否强制编辑,选填) 99 |
发布或编辑群公告
rename_room_name 105 | room_name(群聊名称,必填)
106 | new_name(新群名,必填) 107 |
重命名一个群聊
rename_room_remark 113 | room_name(群聊名称,必填)
114 | new_remark(新备注,必填) 115 |
为群聊设置或修改备注
rename_name_in_room 121 | room_name(群聊名称,必填)
122 | new_name_in_room(新昵称,必填) 123 |
修改“我”在某个群聊中的昵称
leave_room 129 | room_name(群聊名称,必填) 130 | 退出一个群聊
-------------------------------------------------------------------------------- /docs/guide/message-handling.md: -------------------------------------------------------------------------------- 1 | # 消息处理 2 | 3 | Omni Bot SDK 的消息处理机制高度模块化,支持多种消息类型、插件链式处理、上下文注入和自动化响应。核心流程如下: 4 | 5 | ## 1. 消息流转全景 6 | 7 | 1. **消息监听** 8 | SDK 通过数据库轮询准实时监听微信消息。 9 | 2. **消息解析** 10 | 收到原始消息后,使用消息工厂(MessageFactory)自动识别类型(文本、图片、语音、文件、系统等),并构建为统一的消息对象(如 `TextMessage`、`ImageMessage`)。 11 | 3. **上下文注入** 12 | 每条消息会自动注入丰富的上下文信息,包括联系人、群聊、用户信息、数据库句柄等,便于插件开发者灵活处理。 13 | 4. **插件链分发** 14 | 消息对象和上下文会依次传递给所有已启用插件的 `handle_message` 方法。插件可根据优先级排序,链式处理消息。 15 | 5. **中断与响应** 16 | 插件可通过 `should_stop` 机制中断后续插件处理,或通过 `add_response` 返回自动化动作(如回复、RPA操作)。 17 | 6. **异常与去重** 18 | SDK 内部自动处理插件异常,保证单个插件异常不会影响整体消息流转。重复消息、无效消息会被自动过滤。 19 | 20 | ## 2. 支持的消息类型 21 | 22 | SDK 支持微信绝大多数消息类型,包括但不限于: 23 | 24 | - 文本消息(TextMessage) 25 | - 图片消息(ImageMessage) 26 | - 文件消息(FileMessage) 27 | - 语音消息(AudioMessage) 28 | - 视频消息(VideoMessage) 29 | - 表情包(EmojiMessage) 30 | - 链接/小程序/名片/红包/转账/系统消息等 31 | 32 | 每种消息类型都封装为独立的数据类,支持内容解析、格式化、转文本等操作。 33 | 34 | ## 3. 插件链与上下文机制 35 | 36 | - 插件链采用优先级排序,依次调用每个插件的 `handle_message` 方法。 37 | - 每个插件可访问 `PluginExcuteContext`,获取当前消息、上下文、历史响应、错误等。 38 | - 插件可通过 `add_response` 返回自动化动作(如回复消息、触发RPA),也可通过 `should_stop` 中断后续插件处理。 39 | - 插件链支持热重载,开发者可随时增删插件,无需重启主程序。 40 | 41 | ## 4. 典型插件处理流程 42 | 43 | ```python 44 | async def handle_message(self, context: PluginExcuteContext): 45 | message = context.get_message() 46 | # 判断消息类型 47 | if message.local_type == MessageType.Text: 48 | # 处理文本消息 49 | if '你好' in message.content: 50 | context.add_response( 51 | SendTextMessageAction( 52 | content='你好,有什么可以帮您?', 53 | target=message.contact.display_name, 54 | is_chatroom=message.is_chatroom, 55 | ) 56 | ) 57 | context.should_stop = True # 阻止后续插件处理 58 | ``` 59 | 60 | ## 5. 异常与去重机制 61 | 62 | - 插件处理异常会被自动捕获并记录日志,不影响主流程。 63 | - SDK 内部自动去重,防止重复消息被多次处理。 64 | - 支持消息上下文扩展,便于实现多轮对话、上下文感知等高级功能。 65 | 66 | ## 6. 高级用法 67 | 68 | - 支持自定义消息类型扩展 69 | - 支持多线程/异步消息处理,性能优异 70 | - 支持消息队列分发、会话隔离、群聊/私聊自动区分 71 | 72 | --- 73 | 74 | 如需补充具体插件开发示例或消息类型扩展方法,请在github仓库提交issue。 -------------------------------------------------------------------------------- /docs/guide/plugins.md: -------------------------------------------------------------------------------- 1 | # 插件开发 2 | 3 | Omni Bot SDK 支持插件化架构,开发者可以通过自定义插件,扩展机器人的消息处理能力和自动化操作。插件开发简单、灵活,支持热重载和优先级排序。 4 | 5 | ## 插件开发流程 6 | 7 | 1. **继承基类** 8 | 所有插件需继承 `Plugin` 抽象基类(位于 `omni_bot_sdk.plugins.interface`),实现核心接口。 9 | 10 | 2. **实现必要方法** 11 | - `get_plugin_name(self) -> str`:返回插件唯一名称 12 | - `get_plugin_description(self) -> str`:返回插件描述 13 | - `get_priority(self) -> int`:返回插件优先级(数字越大越先执行) 14 | - `get_plugin_config_schema(cls) -> Type[BaseModel]`:返回插件配置的 Pydantic schema 15 | - `async handle_message(self, context: PluginExcuteContext)`:插件的核心消息处理逻辑 16 | 17 | 3. **插件配置** 18 | 插件配置通过 `config.yaml` 的 `plugins` 字段进行集中管理。每个插件可定义自己的配置 schema,自动校验。 19 | 20 | 4. **插件注册与加载** 21 | 插件需通过 Python entry_points 机制注册到 `omni_bot.plugins` 组,或直接放入 SDK 的插件目录。插件管理器会自动发现、加载并按优先级排序。 22 | 23 | 5. **消息处理链** 24 | 插件按优先级依次处理消息。可通过 `context.should_stop = True` 中断后续插件处理。 25 | 26 | ## 插件基类主要接口 27 | 28 | - `handle_message(context: PluginExcuteContext)`:异步消息处理主入口 29 | - `add_rpa_action(action)` / `add_rpa_actions(actions)`:向 RPA 队列添加自动化动作 30 | - `get_plugin_config(key, default)`:获取插件配置项 31 | - `reload_plugin_config()`:热重载插件配置 32 | - `get_plugin_config_schema()`:返回配置 schema(Pydantic) 33 | 34 | ## 最简单的插件开发 Demo 35 | 36 | 下面是一个最简单的“自发消息拦截”插件示例: 37 | 38 | ```python 39 | from omni_bot_sdk.plugins.interface import Plugin, PluginExcuteContext 40 | from pydantic import BaseModel 41 | 42 | class SelfMsgPluginConfig(BaseModel): 43 | enabled: bool = True 44 | priority: int = 1000 45 | 46 | class SelfMsgPlugin(Plugin): 47 | priority = 1000 48 | name = "self-msg-plugin" 49 | 50 | def __init__(self, bot=None): 51 | super().__init__(bot) 52 | self.priority = getattr(self.plugin_config, "priority", self.__class__.priority) 53 | 54 | def get_priority(self) -> int: 55 | return self.priority 56 | 57 | async def handle_message(self, context: PluginExcuteContext): 58 | message = context.get_message() 59 | if message.is_self: 60 | self.logger.info("检测到是自己的消息,直接拦截") 61 | context.should_stop = True 62 | else: 63 | context.should_stop = False 64 | 65 | def get_plugin_name(self) -> str: 66 | return self.name 67 | 68 | def get_plugin_description(self) -> str: 69 | return "拦截自己发送的消息,防止进入后续插件处理" 70 | 71 | @classmethod 72 | def get_plugin_config_schema(cls): 73 | return SelfMsgPluginConfig 74 | ``` 75 | 76 | ## 插件配置示例 77 | 78 | 在 `config.yaml` 中添加: 79 | 80 | ```yaml 81 | plugins: 82 | self-msg-plugin: 83 | enabled: true 84 | priority: 1000 85 | ``` 86 | 87 | ## 开发建议 88 | 89 | - 插件应尽量无副作用,避免阻塞主线程 90 | - 合理设置优先级,避免插件间冲突 91 | - 可通过 `add_rpa_action`/`add_rpa_actions` 触发自动化操作 92 | - 推荐使用类型提示和 Pydantic 进行配置校验 93 | 94 | --- 95 | 96 | 如需更复杂的插件开发示例、插件热重载、插件间通信等进阶内容,请查阅[插件仓库](https://github.com/weixin-omni/omni-bot-plugins-oss)或联系开发者社区。 -------------------------------------------------------------------------------- /docs/guide/quick-start.md: -------------------------------------------------------------------------------- 1 | # 快速开始 2 | 3 | > ⚠️ 重要提醒:RPA 初始化需要扫描窗口并定位,请勿在此期间操作电脑。初始化过程中微信窗口将会自动缩放并移动到屏幕左上角位置。 4 | 5 | > 注意,基于RPA方案,在运行时,请勿操作鼠标键盘,以免影响RPA运行 6 | 7 | ## 环境准备 8 | 9 | **务必使用 Python 3.12** 10 | 11 | ### 获取数据库密钥 12 | 13 | 本项目不提供此工具,可自行通过github获取 DbkeyHookCMD.exe 或 DbkeyHookUI.exe 进行获取。 14 | 获取数据库密钥后,填入配置文件中的 dbkey。 15 | 16 | ### 启动MQTT服务 17 | 18 | mqtt服务用于MCP消息转发,以及后续更新任务执行结果回调,windows可以使用nanomq直接启动本地服务。 19 | 20 | ## 安装 21 | 22 | 通过 pip 从 PyPI 安装: 23 | 24 | ```bash 25 | pip install omni-bot-sdk 26 | ``` 27 | 28 | ## 第一个机器人 29 | 30 | 1. 参考 config.example.yaml 生成 config.yaml 31 | 2. 启动微信并登录 32 | 3. 启动机器人脚本 33 | 4. 机器人会自动给文件传输助手发送一张图片,然后执行获取密钥 34 | 35 | ### Hello, World 示例 36 | 37 | ```python 38 | from omni_bot_sdk.bot import Bot 39 | 40 | def main(): 41 | bot = Bot(config_path="config.yaml") 42 | bot.start() 43 | 44 | if __name__ == "__main__": 45 | main() 46 | ``` 47 | 48 | 现在,去和你的机器人聊天吧! -------------------------------------------------------------------------------- /docs/guide/rpa-operations.md: -------------------------------------------------------------------------------- 1 | # RPA操作 2 | 3 | > ⚠️ 注意:所有 RPA 操作中的 target 参数,均为需要操作的对象“名称”(如好友昵称、群聊名称),而非 ID。由于 RPA 方案无法直接获取微信 ID,请确保名称唯一且准确。 4 | > 5 | > ⚠️ 文件、图片等路径参数必须为本地磁盘的绝对路径,不能为网络URL。请在插件逻辑中提前完成下载等耗时操作,Action 只负责本地自动化。 6 | > 7 | > * 带星号的功能为闭源版本功能,用户需自行开发,开源版本不包含相关代码。 8 | 9 | Omni Bot SDK 内置了丰富的 RPA 操作类型,开发者可以在插件中灵活调用,实现自动化消息、文件、群管理等多种操作。 10 | 11 | ## 可用的 RPA Action Handler 一览 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 |
操作类型Action 类名主要参数说明典型用途
发送文本消息SendTextMessageActioncontent(消息内容), target(对象名称), is_chatroom(是否群聊), at_user_name(@用户名,仅群聊), quote_message(引用消息内容), random_at_quote(是否随机@/引用)发送文字到好友/群
发送图片SendImageActionimage_path(图片路径), target, is_chatroom发送图片到好友/群
发送文件SendFileActionfile_path(文件路径), target, is_chatroom发送文件到好友/群
转发消息ForwardMessageAction(暂无参数,后续可扩展)转发一条消息
下载图片DownloadImageActiontarget(对象名称), max_count(下载数量,默认1)下载图片
下载视频DownloadVideoActiontarget, name(视频名称,可选), max_count(下载数量,默认1), is_chatroom下载视频
下载文件DownloadFileActionfile_url(文件URL), save_path(保存路径)下载文件
拍一拍PatActiontarget(对象名称), user_name(被拍用户昵称,群聊时用), is_chatroom拍一拍好友/群成员
邀请进群*Invite2RoomActionuser_name(被邀请人昵称), target(群聊名称)邀请用户进群
新好友操作*NewFriendActionuser_name(新好友昵称), action(同意/拒绝/忽略), response(拒绝时回复内容), index(可选)同意/拒绝/忽略新好友
发送朋友圈*SendPyqActionimages(图片列表), content(文案)发表朋友圈
群公告PublicRoomAnnouncementActioncontent(公告内容), target(群聊名称), force_edit(是否强制编辑)发布群公告
群成员移除RemoveRoomMemberActionuser_name(被移除成员昵称), target(群聊名称)移除群成员
群名修改RenameRoomNameActiontarget(群聊名称), name(新群名)修改群名
群备注修改RenameRoomRemarkActiontarget(群聊名称), remark(新备注)修改群备注
群昵称修改RenameNameInRoomActiontarget(群聊名称), name(新昵称)修改自己在群的昵称
退出群聊LeaveRoomActiontarget(群聊名称)退出群聊
切换会话SwitchConversationActiontarget(对象名称)切换到指定会话
133 | 134 | > 具体参数和更多操作请参考源码 omni_bot_sdk/rpa/action_handlers/ 目录。 135 | 136 | --- 137 | 138 | ## 在插件中返回RPA操作的示例 139 | 140 | 插件开发者可以在 `handle_message` 方法中,构造对应的 Action 并通过 `add_rpa_action` 或 `add_rpa_actions` 返回。例如: 141 | 142 | ### 发送文本消息 143 | 144 | ```python 145 | from omni_bot_sdk.plugins.interface import Plugin, PluginExcuteContext 146 | from omni_bot_sdk.rpa.action_handlers import SendTextMessageAction 147 | 148 | class DemoPlugin(Plugin): 149 | # ... 省略其它方法 ... 150 | async def handle_message(self, context: PluginExcuteContext): 151 | # 假设收到特定消息时自动回复 152 | message = context.get_message() 153 | if message.text == "你好": 154 | action = SendTextMessageAction( 155 | content="你好,我是机器人!", 156 | target=message.from_user, # 目标用户 157 | is_chatroom=message.is_group # 是否群聊 158 | ) 159 | self.add_rpa_action(action) 160 | ``` 161 | 162 | ### 发送图片 163 | 164 | ```python 165 | from omni_bot_sdk.rpa.action_handlers import SendImageAction 166 | 167 | # ... 在 handle_message 内部 ... 168 | action = SendImageAction( 169 | image_path="/path/to/image.jpg", 170 | target=message.from_user, 171 | is_chatroom=message.is_group 172 | ) 173 | self.add_rpa_action(action) 174 | ``` 175 | 176 | ### 邀请用户进群 177 | 178 | ```python 179 | from omni_bot_sdk.rpa.action_handlers import Invite2RoomAction 180 | 181 | # ... 在 handle_message 内部 ... 182 | action = Invite2RoomAction( 183 | user_name="wxid_xxx", 184 | target="群聊名称" 185 | ) 186 | self.add_rpa_action(action) 187 | ``` 188 | 189 | ### 发送朋友圈 190 | 191 | ```python 192 | from omni_bot_sdk.rpa.action_handlers import SendPyqAction 193 | 194 | # ... 在 handle_message 内部 ... 195 | action = SendPyqAction( 196 | images=["/path/to/img1.jpg", "/path/to/img2.jpg"], 197 | content="自动发朋友圈测试" 198 | ) 199 | self.add_rpa_action(action) 200 | ``` 201 | 202 | --- 203 | 204 | ## 开发建议 205 | 206 | - 每个 Action 类的参数请参考源码注释,确保传递正确。 207 | - 支持批量操作:`self.add_rpa_actions([action1, action2, ...])` 208 | - 插件可组合多种 Action,实现复杂自动化流程。 209 | 210 | 如需更多 Action Handler 的用法和参数说明,请查阅源码或联系开发者社区。 -------------------------------------------------------------------------------- /docs/guide/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # 故障排除 2 | 3 | ## 常见问题 4 | 5 | - RPA操作不准确:请确保微信窗口无干扰,避免同名联系人 6 | - 消息未能及时接收:检查数据库密钥和MQTT服务 7 | - 初始化慢:视觉定位流程正常,耐心等待 8 | 9 | ## 局限性 10 | 详见[RPA操作](/guide/rpa-operations)章节 -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | hero: 5 | name: RPA + VISION 6 | text: 一个基于视觉识别的微信 4.0 RPA框架 7 | actions: 8 | - theme: brand 9 | text: 快速开始 10 | link: /guide/quick-start.html 11 | - theme: alt 12 | text: GitHub 13 | link: https://github.com/weixin-omni/omni-bot-sdk-oss 14 | image: 15 | src: /logo.png 16 | alt: omni-logo 17 | features: 18 | - icon: ⚡️ 19 | title: 无延迟读取 20 | details: 消息获取平均延迟 0.5s 21 | - icon: 🛡️ 22 | title: 运行时零侵入 23 | details: 不注入,不Hook 24 | - icon: 🧩 25 | title: 高效插件扩展 26 | details: 使用插件模式,对接Dify,OpenAI 27 | - icon: 🚀 28 | title: 支持大部分消息类型 29 | details: 已经解析几乎所有消息类型 30 | --- 31 | 32 | 53 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "omni-bot-docs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "author": "Huchundong ", 6 | "license": "CC0-1.0", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+ssh://git@github.com/weixin-omni/omni-bot-sdk-oss.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/weixin-omni/omni-bot-sdk-oss/issues" 13 | }, 14 | "homepage": "https://github.com/weixin-omni/omni-bot-sdk-oss#readme", 15 | "packageManager": "pnpm@10.12.4", 16 | "scripts": { 17 | "dev": "vitepress dev", 18 | "build": "vitepress build", 19 | "serve": "vitepress serve", 20 | "fmt": "prettier --write .", 21 | "fmt:check": "prettier --check ." 22 | }, 23 | "devDependencies": { 24 | "@moefy-canvas/core": "^0.6.0", 25 | "@moefy-canvas/theme-sparkler": "^0.6.0", 26 | "mermaid": "^11.8.1", 27 | "prettier": "^3.5.2", 28 | "vite": "^7.0.0", 29 | "vitepress": "^1.6.3", 30 | "vitepress-plugin-group-icons": "^1.3.6", 31 | "vitepress-plugin-llms": "^1.0.0", 32 | "vitepress-plugin-mermaid": "^2.0.17", 33 | "vue": "^3.5.13" 34 | }, 35 | "pnpm": { 36 | "peerDependencyRules": { 37 | "ignoreMissing": [ 38 | "@algolia/client-search", 39 | "react", 40 | "react-dom", 41 | "@types/react" 42 | ] 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /docs/public/author.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weixin-omni/omni-bot-sdk-oss/7c73235fe5d551a35c396b87a419615847239eec/docs/public/author.jpg -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weixin-omni/omni-bot-sdk-oss/7c73235fe5d551a35c396b87a419615847239eec/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/group1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weixin-omni/omni-bot-sdk-oss/7c73235fe5d551a35c396b87a419615847239eec/docs/public/group1.jpg -------------------------------------------------------------------------------- /docs/public/logo.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:370aaa4ba81f4790ec73104957c07e0248c3012ba48c943e17220ba0ce7d149e 3 | size 95538 4 | -------------------------------------------------------------------------------- /docs/public/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "headers": [ 3 | { 4 | "source": "/assets/(.*)", 5 | "headers": [ 6 | { 7 | "key": "Cache-Control", 8 | "value": "max-age=31536000, immutable" 9 | } 10 | ] 11 | } 12 | ], 13 | "cleanUrls": true 14 | } 15 | -------------------------------------------------------------------------------- /docs/public/zs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weixin-omni/omni-bot-sdk-oss/7c73235fe5d551a35c396b87a419615847239eec/docs/public/zs.jpg -------------------------------------------------------------------------------- /docs/sponsor.md: -------------------------------------------------------------------------------- 1 | # 赞助 2 | 3 | 本项目纯公益,所有核心代码均已开源,任何第三方可使用本项目搭建个人微信机器人,本人不会进行任何商业推广。 4 | 5 | 如果你想支持我的话,在 [GitHub 项目主页](https://github.com/weixin-omni/omni-bot-sdk-oss)给予我一个「Star」就是对我的最大鼓励。 6 | 7 | 此外,如果你想给予我一定资金支持以维持 OMNI-BOT 的持续维护的话,你可以通过以下方式进行: 8 | 9 | ## 一次性赞助 10 | 11 | 你可以通过赞赏 [微信](https://github.com/user-attachments/assets/195ab37d-bc51-44a2-9330-e4df9dbf67dc) 来为 OMNI-BOT 提供一笔开发资金。 12 | 13 | 你的任何金额的赞助我都会无比珍惜,我会在[项目致谢](https://github.com/weixin-omni/omni-bot-sdk-oss/blob/master/README.md)中标注你的 GitHub ID(需要在赞助时备注你的 GitHub ID,如果有资助后忘记留 ID 的可以联系我~)。 14 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "declaration": true, 5 | "declarationMap": true, 6 | "lib": ["DOM", "ES2020"], 7 | "module": "ESNext", 8 | "moduleResolution": "node", 9 | "jsx": "preserve", 10 | "newLine": "lf", 11 | "noEmitOnError": true, 12 | "noImplicitAny": false, 13 | "resolveJsonModule": true, 14 | "skipLibCheck": true, 15 | "sourceMap": true, 16 | "strict": true, 17 | "strictNullChecks": true, 18 | "target": "ES2018" 19 | }, 20 | "include": ["./.vitepress/**/*", "./.vitepress/env.d.ts"] 21 | } 22 | -------------------------------------------------------------------------------- /examples/simple-bot/bot.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.insert(0, os.path.abspath(os.getcwd())) 5 | 6 | try: 7 | from omni_bot_sdk.bot import Bot 8 | except ImportError as e: 9 | print(f"ImportError: {e}") 10 | sys.exit(1) 11 | 12 | 13 | def main(): 14 | bot = Bot(config_path="./config.yaml") 15 | bot.start() 16 | 17 | 18 | if __name__ == "__main__": 19 | main() 20 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # omni-bot-sdk/pyproject.toml 2 | [build-system] 3 | requires = ["setuptools>=61.0"] 4 | build-backend = "setuptools.build_meta" 5 | 6 | [project] 7 | name = "omni-bot-sdk" 8 | version = "2.0.0" 9 | authors = [ 10 | { name="huchundong", email="gycm520@gmail.com" }, 11 | ] 12 | description = "Core SDK for building Omni-channel RPA bots." 13 | readme = "README.md" 14 | requires-python = ">=3.12" 15 | license = "GPL-3.0-or-later" 16 | license-files = ["LICENSE"] 17 | 18 | classifiers = [ 19 | "Programming Language :: Python :: 3.12", 20 | "Operating System :: Microsoft :: Windows", 21 | ] 22 | dependencies = [ 23 | "pyautogui==0.9.54", 24 | "sqlcipher3-wheels==0.5.4.post0", 25 | "ultralytics==8.3.161", 26 | "fastmcp==2.10.1", 27 | "paho-mqtt==2.1.0", 28 | "protobuf==6.31.1", 29 | "aiohttp==3.12.13", 30 | "aiofiles==24.1.0", 31 | "cryptography==45.0.5", 32 | "lxml==6.0.0", 33 | "minio==7.2.15", 34 | "mss==10.0.0", 35 | "fuzzywuzzy==0.18.0", 36 | "xmltodict==0.14.2", 37 | "yara-python==4.5.4", 38 | "zstandard==0.23.0", 39 | "pymem==1.14.0", 40 | "pywin32==310", 41 | "rapidocr==3.2.0", 42 | "python-Levenshtein==0.27.1", 43 | "watchfiles==1.1.0", 44 | "onnxruntime==1.22.0", 45 | "ruamel.yaml==0.18.14", 46 | "pydantic==2.11.7", 47 | "py-machineid==0.8.0", 48 | "pyjwt==2.10.1", 49 | "openai==1.93.0", 50 | "boto3==1.39.3", 51 | ] 52 | [project.urls] 53 | Homepage = "https://github.com/weixin-omni/omni-bot-sdk-oss" 54 | Issues = "https://github.com/weixin-omni/omni-bot-sdk-oss/issues" 55 | 56 | [tool.setuptools] 57 | package-dir = {"" = "src"} 58 | 59 | [tool.deptry] 60 | per_rule_ignores = { "DEP003" = ["omni_bot_sdk"] } 61 | 62 | [tool.setuptools.package-data] 63 | "omni_bot_sdk.yolo.models" = ["*.pt"] 64 | # =================================================================== 65 | # =================== 核心插件入口点配置 ====================== 66 | # =================================================================== 67 | [project.entry-points."omni_bot.plugins"] 68 | # "入口点名称" = "python.import.path:ClassName" 69 | # - "omni_bot.plugins" 是你定义的入口点组,PluginManager会搜索这个组。 70 | # - "入口点名称" 是插件的唯一标识符。这个名字会用于配置文件的启用/禁用。 71 | # - "omni_bot_sdk.plugins.core.self_msg_plugin:SelfMsgPlugin" 是插件类的完整导入路径。 72 | 73 | self-msg-plugin = "omni_bot_sdk.plugins.core.self_msg_plugin:SelfMsgPlugin" 74 | block-empty-room-plugin = "omni_bot_sdk.plugins.core.block_empty_room_plugin:BlockEmptyRoomPlugin" 75 | image-aes-plugin = "omni_bot_sdk.plugins.core.image_aes_plugin:ImageAesPlugin" 76 | -------------------------------------------------------------------------------- /scripts/update_version_and_tag.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | 自动更新版本号、打tag并推送的脚本 4 | 用法:python scripts/update_version_and_tag.py 1.2.3 5 | """ 6 | import sys 7 | import re 8 | import subprocess 9 | from pathlib import Path 10 | 11 | 12 | def update_version(new_version): 13 | pyproject = Path("pyproject.toml") 14 | if not pyproject.exists(): 15 | print("pyproject.toml 不存在") 16 | sys.exit(1) 17 | content = pyproject.read_text(encoding="utf-8") 18 | new_content = re.sub( 19 | r'version\s*=\s*"[^"]*"', f'version = "{new_version}"', content 20 | ) 21 | pyproject.write_text(new_content, encoding="utf-8") 22 | print(f"已更新 pyproject.toml 版本号为 {new_version}") 23 | 24 | 25 | def run(cmd): 26 | print(f"执行: {cmd}") 27 | result = subprocess.run(cmd, shell=True) 28 | if result.returncode != 0: 29 | print(f"命令失败: {cmd}") 30 | sys.exit(1) 31 | 32 | 33 | def main(): 34 | if len(sys.argv) != 2: 35 | print("用法: python scripts/update_version_and_tag.py 1.2.3") 36 | sys.exit(1) 37 | version = sys.argv[1] 38 | if not re.match(r"^\d+\.\d+\.\d+$", version): 39 | print("版本号格式错误,应为 x.y.z") 40 | sys.exit(1) 41 | update_version(version) 42 | run("git add pyproject.toml") 43 | run(f'git commit -m "Bump version to {version}"') 44 | run(f"git tag v{version}") 45 | run("git push") 46 | run("git push --tags") 47 | print(f"已提交、打tag并推送 v{version}") 48 | 49 | 50 | if __name__ == "__main__": 51 | main() 52 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | WeixinOmni 项目主模块。 3 | 本包为微信全能机器人SDK的核心实现,包含所有基础能力、接口与扩展点。 4 | """ 5 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/clients/mqtt_client.py: -------------------------------------------------------------------------------- 1 | """ 2 | MQTT客户端模块 3 | """ 4 | 5 | import json 6 | import logging 7 | import time 8 | from typing import Callable, Optional 9 | 10 | import paho.mqtt.client as mqtt 11 | 12 | 13 | class MQTTClient: 14 | """ 15 | MQTT 客户端封装。 16 | 支持自动重连、消息回调、主题订阅与发布等常用功能。 17 | """ 18 | 19 | def __init__( 20 | self, 21 | host: str, 22 | port: int, 23 | client_id: str, 24 | username: str = None, 25 | password: str = None, 26 | ): 27 | """ 28 | 初始化MQTT客户端。 29 | 支持用户名密码认证,自动生成唯一client_id。 30 | """ 31 | self.client_id = client_id + "_" + str(time.time()) 32 | self.client = mqtt.Client(client_id=self.client_id) 33 | self.host = host 34 | self.port = port 35 | self.username = username 36 | self.password = password 37 | self.message_callback: Optional[Callable] = None 38 | self.logger = logging.getLogger(__name__) 39 | self.logger.info(f"MQTTClient initialized with client_id: {self.client_id}") 40 | if username and password: 41 | self.client.username_pw_set(username, password) 42 | self.logger.info(f"已设置MQTT认证信息 - 用户名: {username}") 43 | 44 | self.client.on_connect = self.on_connect 45 | self.client.on_disconnect = self.on_disconnect 46 | self.client.on_message = self.on_message 47 | 48 | # 设置自动重连延迟 49 | self.client.reconnect_delay_set(min_delay=1, max_delay=120) 50 | 51 | def on_connect(self, client, userdata, flags, rc): 52 | """ 53 | 连接回调。 54 | 连接成功/失败时自动触发。 55 | """ 56 | if rc == 0: 57 | self.logger.info(f"MQTT连接成功:{self.client_id}") 58 | client.connected_flag = True 59 | client.bad_connection_flag = False 60 | else: 61 | self.logger.error(f"MQTT连接失败,错误码: {rc}") 62 | client.bad_connection_flag = True 63 | if rc == 7: 64 | self.logger.error("MQTT认证失败,请检查用户名和密码是否正确") 65 | 66 | def on_disconnect(self, client, userdata, rc): 67 | """ 68 | 断开连接回调。 69 | 包含自动重连逻辑。 70 | """ 71 | self.logger.info("MQTT连接断开") 72 | client.connected_flag = False 73 | if rc != 0: 74 | self.logger.error(f"意外断开连接,错误码: {rc}") 75 | client.bad_connection_flag = True 76 | try: 77 | self.connect() 78 | except Exception as e: 79 | self.logger.error(f"自动重连失败: {e}") 80 | 81 | def on_message(self, client, userdata, msg): 82 | """ 83 | 消息接收回调。 84 | 自动解码JSON消息并调用用户自定义回调。 85 | """ 86 | try: 87 | payload = json.loads(msg.payload.decode()) 88 | if self.message_callback: 89 | self.message_callback({"topic": msg.topic, "payload": payload}) 90 | except Exception as e: 91 | self.logger.error(f"处理MQTT消息时出错: {e}") 92 | 93 | def connect(self): 94 | """ 95 | 连接到MQTT服务器。 96 | 支持匿名连接和认证连接。 97 | """ 98 | if not (self.username and self.password): 99 | self.logger.warning("未提供MQTT认证信息,尝试匿名连接") 100 | try: 101 | self.client.connect(self.host, self.port, keepalive=60) 102 | self.client.loop_start() 103 | except Exception as e: 104 | self.logger.error(f"MQTT连接失败: {e}") 105 | raise 106 | 107 | def subscribe(self, topic: str): 108 | """ 109 | 订阅指定主题。 110 | """ 111 | self.client.subscribe(topic) 112 | self.logger.info(f"已订阅主题: {topic}") 113 | 114 | def publish(self, topic: str, payload: dict): 115 | """ 116 | 发布消息到指定主题。 117 | """ 118 | try: 119 | self.client.publish(topic, json.dumps(payload)) 120 | except Exception as e: 121 | self.logger.error(f"发布MQTT消息时出错: {e}") 122 | 123 | def set_message_callback(self, callback: Callable): 124 | """ 125 | 设置自定义消息回调函数。 126 | 回调参数为dict,包含topic和payload。 127 | """ 128 | self.message_callback = callback 129 | 130 | def disconnect(self): 131 | """ 132 | 断开与MQTT服务器的连接。 133 | """ 134 | self.client.loop_stop() 135 | self.client.disconnect() 136 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/common/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | common 公共模块包。 3 | 包含配置、异常、队列等通用基础能力。 4 | """ 5 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/common/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | 配置加载与访问模块 3 | """ 4 | 5 | from pathlib import Path 6 | from typing import Any 7 | 8 | from ruamel.yaml import YAML 9 | 10 | 11 | class Config: 12 | """ 13 | 配置管理类。 14 | 支持YAML配置文件的加载、嵌套访问和字典式访问。 15 | """ 16 | 17 | def __init__(self, config_path: str = "config.yaml"): 18 | self.config_path = Path(config_path) 19 | self.config = self._load_config() 20 | 21 | def _load_config(self): 22 | """ 23 | 加载YAML配置文件。 24 | """ 25 | if not self.config_path.exists(): 26 | raise FileNotFoundError(f"配置文件 {self.config_path} 不存在") 27 | 28 | with open(self.config_path, "r", encoding="utf-8") as f: 29 | return YAML().load(f) 30 | 31 | def get(self, key: str, default: Any = None) -> Any: 32 | """ 33 | 获取配置项,支持点号分隔的嵌套访问。 34 | 35 | Args: 36 | key: 配置键,支持点号分隔的嵌套键,如 'plugins.my_plugin' 37 | default: 默认值,当配置项不存在时返回 38 | 39 | Returns: 40 | 配置项的值,如果不存在则返回默认值 41 | """ 42 | if "." not in key: 43 | return self.config.get(key, default) 44 | 45 | # 处理嵌套键 46 | keys = key.split(".") 47 | value = self.config 48 | 49 | for k in keys: 50 | if not isinstance(value, dict): 51 | return default 52 | value = value.get(k) 53 | if value is None: 54 | return default 55 | 56 | return value 57 | 58 | def set(self, key: str, value: Any): 59 | """ 60 | 设置配置项,支持点号分隔的嵌套访问。 61 | """ 62 | if "." not in key: 63 | self.config[key] = value 64 | with open(self.config_path, "w", encoding="utf-8") as f: 65 | YAML().dump(self.config, f) 66 | return 67 | 68 | keys = key.split(".") 69 | value = self.config 70 | for k in keys: 71 | if not isinstance(value, dict): 72 | return 73 | value[keys[-1]] = value 74 | with open(self.config_path, "w", encoding="utf-8") as f: 75 | YAML().dump(self.config, f) 76 | 77 | def __getitem__(self, key: str) -> Any: 78 | """ 79 | 支持字典式访问配置项。 80 | """ 81 | return self.config[key] 82 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/common/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | 自定义异常类模块 3 | """ 4 | 5 | 6 | class WeixinOmniError(Exception): 7 | """ 8 | Omni-Bot项目的基础异常类。 9 | 所有自定义异常均应继承自该类,便于统一捕获和处理。 10 | """ 11 | 12 | pass 13 | 14 | 15 | class ConfigError(WeixinOmniError): 16 | """ 17 | 配置相关错误。 18 | 用于配置文件缺失、格式错误等场景。 19 | """ 20 | 21 | pass 22 | 23 | 24 | class DatabaseError(WeixinOmniError): 25 | """ 26 | 数据库相关错误。 27 | 用于数据库连接、查询、操作等异常。 28 | """ 29 | 30 | pass 31 | 32 | 33 | class MQTTError(WeixinOmniError): 34 | """ 35 | MQTT相关错误。 36 | 用于MQTT连接、消息发布/订阅等异常。 37 | """ 38 | 39 | pass 40 | 41 | 42 | class RPAError(WeixinOmniError): 43 | """ 44 | RPA相关错误。 45 | 用于RPA流程、任务等异常。 46 | """ 47 | 48 | pass 49 | 50 | 51 | class WorkerError(WeixinOmniError): 52 | """ 53 | Worker相关错误。 54 | 用于多进程/多线程Worker异常。 55 | """ 56 | 57 | pass 58 | 59 | 60 | class APIError(WeixinOmniError): 61 | """ 62 | API相关错误。 63 | 用于接口调用、参数校验、状态码异常等。 64 | """ 65 | 66 | def __init__(self, message: str, status_code: int = 500): 67 | super().__init__(message) 68 | self.status_code = status_code 69 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/common/queues.py: -------------------------------------------------------------------------------- 1 | """ 2 | 共享队列实例模块 3 | """ 4 | 5 | from queue import Queue 6 | from typing import Any, Dict 7 | 8 | # 全局消息队列,供消息分发与处理模块使用 9 | message_queue = Queue() 10 | 11 | # 全局RPA任务队列,供RPA相关服务使用 12 | rpa_task_queue = Queue() 13 | 14 | # 全局状态队列,用于状态变更通知 15 | status_queue = Queue() 16 | 17 | 18 | def get_queue_stats() -> Dict[str, Any]: 19 | """ 20 | 获取所有全局队列的状态信息(队列长度及是否为空)。 21 | """ 22 | return { 23 | "message_queue": { 24 | "size": message_queue.qsize(), 25 | "empty": message_queue.empty(), 26 | }, 27 | "rpa_task_queue": { 28 | "size": rpa_task_queue.qsize(), 29 | "empty": rpa_task_queue.empty(), 30 | }, 31 | "status_queue": {"size": status_queue.qsize(), "empty": status_queue.empty()}, 32 | } 33 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/mcp/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MCP (Message Control Panel) 模块包 3 | """ 4 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/mcp/dispatchers.py: -------------------------------------------------------------------------------- 1 | # mcp/dispatchers.py 2 | import time 3 | from typing import Any, Dict 4 | 5 | from omni_bot_sdk.clients.mqtt_client import MQTTClient 6 | from omni_bot_sdk.models import UserInfo 7 | from omni_bot_sdk.rpa.action_handlers import RPAActionType 8 | 9 | 10 | class MqttCommandDispatcher: 11 | """ 12 | 使用MQTT实现的命令分发器。 13 | 支持通用消息分发和RPA操作分发。 14 | """ 15 | 16 | def __init__(self, mqtt_client: MQTTClient, user_info: UserInfo): 17 | """ 18 | 初始化分发器,注入MQTT客户端和用户信息。 19 | """ 20 | self.mqtt = mqtt_client 21 | self.user = user_info 22 | 23 | def dispatch(self, topic: str, payload: Dict[str, Any]) -> None: 24 | """ 25 | 发送通用MQTT消息。 26 | 检查MQTT连接状态,异常时抛出错误。 27 | """ 28 | if not self.mqtt.client.connected_flag or self.mqtt.client.bad_connection_flag: 29 | raise ConnectionError("MQTT连接不可用,请检查MQTT服务状态。") 30 | self.mqtt.publish(topic, payload) 31 | 32 | def dispatch_rpa(self, action_type: str, action_data: Dict[str, Any]) -> str: 33 | """ 34 | 发送RPA操作到专用MQTT主题。 35 | 返回操作提交结果字符串。 36 | """ 37 | topic = f"msg/{self.user.account}/other_rpa_action" 38 | payload = { 39 | "create_time": int(time.time()), 40 | "action_type": action_type, 41 | "action_data": action_data, 42 | } 43 | self.dispatch(topic, payload) 44 | return f"RPA操作 '{str(action_type)}' 已成功提交。" 45 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/mcp/protocols.py: -------------------------------------------------------------------------------- 1 | # mcp/protocols.py 2 | from typing import Any, Dict, Protocol 3 | 4 | 5 | class CommandDispatcher(Protocol): 6 | """ 7 | 命令分发器协议。 8 | 统一定义消息分发接口,便于不同实现(如MQTT、队列等)互换。 9 | """ 10 | 11 | def dispatch(self, topic: str, payload: Dict[str, Any]) -> None: 12 | """ 13 | 分发一个通用的消息。 14 | topic: 目标主题或通道 15 | payload: 消息内容 16 | """ 17 | ... 18 | 19 | def dispatch_rpa(self, action_type: str, action_data: Dict[str, Any]) -> str: 20 | """ 21 | 分发一个RPA操作,并返回确认信息。 22 | action_type: 操作类型 23 | action_data: 操作参数 24 | """ 25 | ... 26 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 插件包 3 | """ 4 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/plugins/core/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 插件包 3 | """ 4 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/plugins/core/block_empty_room_plugin.py: -------------------------------------------------------------------------------- 1 | """ 2 | 群聊重命名插件 3 | 4 | 该插件负责处理群聊名称相关的功能。 5 | 主要功能: 6 | - 检测群聊名称是否为空 7 | - 处理群聊重命名相关的操作 8 | - 提供群聊名称管理的响应 9 | 10 | 注意事项: 11 | - 该插件配置为高优先级(999),确保群聊名称能够被及时处理 12 | - 需要确保群聊名称的合法性 13 | - 需要处理群聊重命名失败的情况 14 | """ 15 | 16 | from omni_bot_sdk.plugins.interface import Bot, Plugin, PluginExcuteContext 17 | from pydantic import BaseModel 18 | 19 | 20 | class BlockEmptyRoomPluginConfig(BaseModel): 21 | """ 22 | 群聊重命名插件配置 23 | enabled: 是否启用该插件 24 | priority: 插件优先级,数值越大优先级越高 25 | """ 26 | 27 | enabled: bool = False 28 | priority: int = 998 29 | 30 | 31 | class BlockEmptyRoomPlugin(Plugin): 32 | """ 33 | 群聊重命名插件实现类 34 | """ 35 | 36 | priority = 998 37 | name = "block-empty-room-plugin" 38 | 39 | def __init__(self, bot: "Bot" = None): 40 | # 设置视频文件保存路径 41 | super().__init__(bot) 42 | # 动态优先级支持 43 | self.priority = getattr(self.plugin_config, "priority", self.__class__.priority) 44 | 45 | def get_priority(self) -> int: 46 | return self.priority 47 | 48 | async def handle_message(self, context: PluginExcuteContext) -> None: 49 | """ 50 | 处理接收到的消息 51 | 52 | 参数: 53 | context (PluginExcuteContext): 消息处理上下文信息 54 | 55 | 返回: 56 | None: 处理结果通过context.add_response()方法返回 57 | """ 58 | message = context.get_message() 59 | 60 | if message.is_chatroom and ( 61 | message.room.nick_name is None or message.room.nick_name == "" 62 | ): 63 | self.logger.warn("当前群没有备注或名称,无法定位,请手动设置群名称") 64 | self.logger.warn(message) 65 | # TODO 可以发送修改请求,较复杂,后续开发 66 | context.should_stop = True 67 | 68 | def get_plugin_name(self) -> str: 69 | return self.name 70 | 71 | def get_plugin_description(self) -> str: 72 | return "这是一个用于给新创建的群修改备注的插件" 73 | 74 | @classmethod 75 | def get_plugin_config_schema(cls): 76 | """ 77 | 返回插件配置的pydantic schema类。 78 | """ 79 | return BlockEmptyRoomPluginConfig 80 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/plugins/core/image_aes_plugin.py: -------------------------------------------------------------------------------- 1 | """ 2 | 辅助查找图片AES密钥 3 | """ 4 | 5 | from typing import TYPE_CHECKING 6 | from omni_bot_sdk.plugins.interface import ( 7 | Bot, 8 | Plugin, 9 | PluginExcuteContext, 10 | PluginExcuteResponse, 11 | DownloadImageAction, 12 | MessageType, 13 | ) 14 | from pydantic import BaseModel 15 | 16 | if TYPE_CHECKING: 17 | from omni_bot_sdk.bot import Bot 18 | 19 | 20 | class ImageAesPluginConfig(BaseModel): 21 | """ 22 | 群聊重命名插件配置 23 | enabled: 是否启用该插件 24 | priority: 插件优先级,数值越大优先级越高 25 | """ 26 | 27 | enabled: bool = True 28 | priority: int = 2000 29 | 30 | 31 | class ImageAesPlugin(Plugin): 32 | """ 33 | 群聊重命名插件实现类 34 | """ 35 | 36 | priority = 2000 37 | name = "image-aes-plugin" 38 | 39 | def __init__(self, bot: "Bot" = None): 40 | super().__init__(bot) 41 | # 动态优先级支持 42 | self.priority = getattr(self.plugin_config, "priority", self.__class__.priority) 43 | 44 | def get_priority(self) -> int: 45 | return self.priority 46 | 47 | async def handle_message(self, context: PluginExcuteContext) -> None: 48 | """ 49 | 处理接收到的消息 50 | 51 | 参数: 52 | context (PluginExcuteContext): 消息处理上下文信息 53 | 54 | 返回: 55 | None: 处理结果通过context.add_response()方法返回 56 | """ 57 | message = context.get_message() 58 | 59 | if message.local_type == MessageType.Image and message.is_self: 60 | # 初始化,自己发给自己一张图片 61 | if not self.bot.dat_decrypt_service._init_done: 62 | self.logger.info("图片解密服务未初始化,启动延迟初始化操作") 63 | self.bot.dat_decrypt_service.setup_lazy() 64 | 65 | def get_plugin_name(self) -> str: 66 | return self.name 67 | 68 | def get_plugin_description(self) -> str: 69 | return "这是一个用于给新创建的群修改备注的插件" 70 | 71 | @classmethod 72 | def get_plugin_config_schema(cls): 73 | """ 74 | 返回插件配置的pydantic schema类。 75 | """ 76 | return ImageAesPluginConfig 77 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/plugins/core/plugin_interface.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from abc import ABC, abstractmethod 3 | from queue import Queue 4 | from typing import TYPE_CHECKING, Any, Dict, List, Type 5 | import json 6 | 7 | from omni_bot_sdk.rpa.action_handlers import RPAAction 8 | from omni_bot_sdk.weixin.message_classes import Message 9 | from pydantic import BaseModel, ValidationError 10 | 11 | if TYPE_CHECKING: 12 | from omni_bot_sdk.bot import Bot 13 | 14 | 15 | class PluginExcuteResponse: 16 | """ 17 | 插件处理结果对象。 18 | 用于收集插件处理消息后的响应、动作、状态等。 19 | """ 20 | 21 | plugin_name: str 22 | handled: bool 23 | should_stop: bool 24 | response: Dict[str, Any] 25 | actions: List[RPAAction] 26 | message: Message 27 | 28 | def __init__( 29 | self, 30 | plugin_name: str, 31 | handled: bool = False, 32 | should_stop: bool = False, 33 | response: Dict[str, Any] = None, 34 | actions: List[RPAAction] = None, 35 | message: Message = None, 36 | ): 37 | self.plugin_name = plugin_name 38 | self.handled = handled 39 | self.should_stop = should_stop 40 | self.response = response or {} 41 | self.actions = actions or [] 42 | self.message = message 43 | 44 | def add_action(self, action: RPAAction): 45 | """ 46 | 添加一个RPA动作到响应。 47 | """ 48 | self.actions.append(action) 49 | 50 | def get_actions(self) -> List[RPAAction]: 51 | """ 52 | 获取所有RPA动作。 53 | """ 54 | return self.actions or [] 55 | 56 | 57 | class PluginExcuteContext: 58 | """ 59 | 插件执行上下文。 60 | 封装消息、上下文、响应、错误、should_stop等。 61 | """ 62 | 63 | message: Message 64 | context: dict 65 | responses: List[PluginExcuteResponse] 66 | errors: List[Exception] 67 | should_stop: bool 68 | 69 | def __init__(self, message: Message, context: dict): 70 | self.message = message 71 | self.context = context 72 | self.errors = [] 73 | self.responses = [] 74 | self.should_stop = False 75 | 76 | def get_message(self) -> Message: 77 | """ 78 | 获取当前消息对象。 79 | """ 80 | return self.message 81 | 82 | def get_context(self) -> dict: 83 | """ 84 | 获取当前上下文。 85 | """ 86 | return self.context 87 | 88 | def add_error(self, plugin_name: str, error_message: str): 89 | """ 90 | 添加插件处理错误。 91 | """ 92 | self.errors.append(f"Error in {plugin_name}: {error_message}") 93 | # 可选:可在此处设置 should_stop = True 94 | 95 | def add_response(self, value: PluginExcuteResponse): 96 | """ 97 | 添加插件处理响应。 98 | """ 99 | self.responses.append(value) 100 | 101 | def get_responses(self) -> List[PluginExcuteResponse]: 102 | """ 103 | 获取所有插件响应。 104 | """ 105 | return self.responses 106 | 107 | def should_stop(self) -> bool: 108 | """ 109 | 是否中断后续插件处理。 110 | """ 111 | return self.should_stop 112 | 113 | 114 | class Plugin(ABC): 115 | """ 116 | 异步插件抽象基类。 117 | 所有插件必须继承自此类,实现核心接口。 118 | """ 119 | 120 | priority: int = 0 # 插件执行优先级,数字越大越先执行 121 | 122 | def __init__(self, bot: "Bot"): 123 | self.bot = bot 124 | self.logger = bot.logger 125 | self.config = bot.config 126 | self.rpa_queue = bot.rpa_task_queue 127 | self.plugin_config = None 128 | self.reload_plugin_config() 129 | 130 | def _load_plugin_config(self): 131 | """ 132 | 加载插件特定配置。 133 | """ 134 | plugin_name = self.get_plugin_name() 135 | plugins_config = self.config.get("plugins", {}) 136 | return plugins_config.get(plugin_name, {}) 137 | 138 | def reload_plugin_config(self): 139 | """ 140 | 重新加载并校验插件配置,支持热重载。 141 | """ 142 | raw_config = self._load_plugin_config() 143 | schema = self.get_plugin_config_schema() 144 | try: 145 | validated_config = schema(**raw_config) 146 | except ValidationError as e: 147 | self.logger.error(f"插件 {self.get_plugin_name()} 配置校验失败: {e}") 148 | raise 149 | self.plugin_config = validated_config 150 | return self.plugin_config 151 | 152 | def get_plugin_config(self, key, default=None): 153 | """ 154 | 获取插件配置项(已校验后的pydantic对象)。 155 | 支持点号和dict访问。 156 | """ 157 | if self.plugin_config is None: 158 | self.reload_plugin_config() 159 | if hasattr(self.plugin_config, key): 160 | return getattr(self.plugin_config, key, default) 161 | return getattr(self.plugin_config, "__dict__", {}).get(key, default) 162 | 163 | def add_rpa_action(self, action: RPAAction): 164 | """ 165 | 添加单个RPA动作到队列。 166 | Args: 167 | action: RPA动作对象 168 | """ 169 | self.add_rpa_actions([action]) 170 | 171 | def add_rpa_actions(self, actions: List[RPAAction]): 172 | """ 173 | 批量添加RPA动作到队列。 174 | 线程安全由ProcessorService保证。 175 | """ 176 | if not actions: 177 | return 178 | self.bot.processor_service.add_rpa_actions(actions) 179 | 180 | @classmethod 181 | @abstractmethod 182 | def get_plugin_config_schema(cls) -> Type[BaseModel]: 183 | """ 184 | 返回插件配置的pydantic schema类。 185 | """ 186 | raise NotImplementedError 187 | 188 | def get_validated_plugin_config(self) -> BaseModel: 189 | """ 190 | 获取并校验当前插件配置,返回pydantic模型对象。 191 | """ 192 | if self.plugin_config is None: 193 | self.reload_plugin_config() 194 | return self.plugin_config 195 | 196 | def get_plugin_config_info(self) -> Dict[str, Any]: 197 | """ 198 | 获取插件配置schema和当前配置。 199 | Returns: 200 | dict: { 'schema': schema_json, 'config': current_config } 201 | """ 202 | schema = self.get_plugin_config_schema() 203 | config_dict = self.plugin_config.model_dump() if self.plugin_config else {} 204 | return { 205 | "schema": json.dumps(schema.model_json_schema(), ensure_ascii=False), 206 | "config": config_dict, 207 | } 208 | 209 | @abstractmethod 210 | def get_priority(self) -> int: 211 | """ 212 | 获取插件优先级。 213 | """ 214 | return 0 215 | 216 | @abstractmethod 217 | async def handle_message(self, context: PluginExcuteContext): 218 | """ 219 | 处理消息的异步方法。 220 | 插件的核心逻辑在这里实现。 221 | """ 222 | raise NotImplementedError 223 | 224 | @abstractmethod 225 | def get_plugin_name(self) -> str: 226 | """ 227 | 返回插件的唯一名称。 228 | """ 229 | raise NotImplementedError 230 | 231 | @abstractmethod 232 | def get_plugin_description(self) -> str: 233 | """ 234 | 获取插件描述 235 | """ 236 | raise NotImplementedError 237 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/plugins/core/self_msg_plugin.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import TYPE_CHECKING 3 | from pydantic import BaseModel 4 | 5 | from omni_bot_sdk.plugins.interface import Plugin, PluginExcuteContext 6 | 7 | if TYPE_CHECKING: 8 | from omni_bot_sdk.bot import Bot 9 | 10 | 11 | class SelfMsgPluginConfig(BaseModel): 12 | """ 13 | 自我消息插件配置 14 | enabled: 是否启用该插件 15 | priority: 插件优先级,数值越大优先级越高 16 | """ 17 | 18 | enabled: bool = False 19 | priority: int = 1000 20 | 21 | 22 | class SelfMsgPlugin(Plugin): 23 | """ 24 | 自我消息处理插件实现类 25 | 26 | 继承自Plugin基类,用于处理用户自己发送的消息。 27 | 作为消息处理链中的第一个插件,用于拦截用户自己发送的消息,防止这些消息进入后续处理流程。 28 | 29 | 属性: 30 | priority (int): 插件优先级,设置为1000确保最先执行 31 | name (str): 插件名称标识符 32 | """ 33 | 34 | priority = 1000 35 | name = "self-msg-plugin" 36 | 37 | def __init__(self, bot: "Bot" = None): 38 | super().__init__(bot) 39 | # 动态优先级支持 40 | self.priority = getattr(self.plugin_config, "priority", self.__class__.priority) 41 | 42 | def get_priority(self) -> int: 43 | return self.priority 44 | 45 | async def handle_message(self, plusginExcuteContext: PluginExcuteContext) -> None: 46 | message = plusginExcuteContext.get_message() 47 | context = plusginExcuteContext.get_context() 48 | if message.is_self: 49 | self.logger.info("检测到是自己的消息,直接拦截,不再让后续的处理") 50 | plusginExcuteContext.should_stop = True 51 | else: 52 | plusginExcuteContext.should_stop = False 53 | 54 | def get_plugin_name(self) -> str: 55 | return self.name 56 | 57 | def get_plugin_description(self) -> str: 58 | return "这是一个用于处理用户自己发送消息的插件,用于拦截自己发送的消息" 59 | 60 | @classmethod 61 | def get_plugin_config_schema(cls): 62 | """ 63 | 返回插件配置的pydantic schema类。 64 | """ 65 | return SelfMsgPluginConfig 66 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/plugins/plugin_manager.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata 2 | import logging 3 | from queue import Queue 4 | from typing import TYPE_CHECKING, Dict, List 5 | 6 | # TYPE_CHECKING块仅用于类型提示,避免运行时循环依赖。 7 | if TYPE_CHECKING: 8 | from omni_bot_sdk.bot import Bot 9 | from omni_bot_sdk.weixin.message_classes import Message 10 | 11 | from omni_bot_sdk.plugins.core.plugin_interface import ( 12 | Plugin, 13 | PluginExcuteContext, 14 | PluginExcuteResponse, 15 | ) 16 | 17 | # 插件入口点组名,所有插件需注册到该组。 18 | PLUGIN_ENTRY_POINT_GROUP = "omni_bot.plugins" 19 | 20 | 21 | class PluginManager: 22 | """ 23 | 插件管理器。 24 | 负责插件的自动发现、加载、优先级排序、消息分发与热重载。 25 | """ 26 | 27 | def __init__(self, bot: "Bot"): 28 | """ 29 | 初始化插件管理器。 30 | Args: 31 | bot (Bot): 主Bot实例,将注入到每个插件。 32 | """ 33 | self.logger = logging.getLogger(__name__) 34 | self.bot = bot 35 | self.plugins: List[Plugin] = [] 36 | 37 | def setup(self): 38 | """ 39 | 初始化并加载所有插件。 40 | """ 41 | self.load_plugins() 42 | 43 | def load_plugins(self): 44 | """ 45 | 发现并加载所有已安装插件。 46 | 支持插件启用/禁用配置,自动注入Bot实例。 47 | 加载后按优先级排序。 48 | """ 49 | self.logger.info(f"开始通过入口点组 '{PLUGIN_ENTRY_POINT_GROUP}' 加载插件...") 50 | 51 | plugins_config = self.bot.config.get("plugins", {}) 52 | 53 | try: 54 | discovered_plugins = importlib.metadata.entry_points( 55 | group=PLUGIN_ENTRY_POINT_GROUP 56 | ) 57 | except AttributeError: 58 | discovered_plugins = importlib.metadata.entry_points().get( 59 | PLUGIN_ENTRY_POINT_GROUP, [] 60 | ) 61 | 62 | if not discovered_plugins: 63 | self.logger.warning("未发现任何已安装的插件。请确保插件包已正确安装。") 64 | 65 | for entry_point in discovered_plugins: 66 | plugin_id = entry_point.name 67 | try: 68 | plugin_conf = plugins_config.get(plugin_id, {}) 69 | if ( 70 | isinstance(plugin_conf, dict) 71 | and plugin_conf.get("enabled", False) is False 72 | ): 73 | self.logger.info(f"插件 '{plugin_id}' 在配置中被禁用,跳过加载。") 74 | continue 75 | 76 | self.logger.debug( 77 | f"正在加载插件 '{plugin_id}' from '{entry_point.value}'..." 78 | ) 79 | 80 | plugin_class = entry_point.load() 81 | 82 | if not ( 83 | isinstance(plugin_class, type) and issubclass(plugin_class, Plugin) 84 | ): 85 | self.logger.warning( 86 | f"入口点 '{plugin_id}' 指向的对象不是有效的 Plugin 子类,已跳过。" 87 | ) 88 | continue 89 | 90 | plugin_instance = plugin_class(self.bot) 91 | self.plugins.append(plugin_instance) 92 | self.logger.info( 93 | f"成功加载并实例化插件: {plugin_instance.get_plugin_name()} (ID: {plugin_id})" 94 | ) 95 | 96 | except Exception as e: 97 | self.logger.error( 98 | f"加载插件 '{plugin_id}' 时发生错误: {e}", exc_info=True 99 | ) 100 | 101 | # 按插件优先级降序排序 102 | self.plugins.sort(key=lambda p: getattr(p, "priority", 0), reverse=True) 103 | 104 | if self.plugins: 105 | plugin_order = " -> ".join([p.get_plugin_name() for p in self.plugins]) 106 | self.logger.info(f"插件加载完成,执行顺序: {plugin_order}") 107 | else: 108 | self.logger.info("插件加载完成,但没有活动的插件。") 109 | 110 | async def process_message( 111 | self, message: "Message", context: Dict 112 | ) -> List[PluginExcuteResponse]: 113 | """ 114 | 异步处理消息,依次调用每个插件的 async handle_message 方法。 115 | 支持should_stop机制,遇到插件中断链路时提前终止。 116 | """ 117 | excute_context = PluginExcuteContext(message, context) 118 | for plugin in self.plugins: 119 | try: 120 | await plugin.handle_message(excute_context) 121 | self.logger.debug(f"插件 '{plugin.get_plugin_name()}' 处理完成。") 122 | if excute_context.should_stop: 123 | self.logger.info( 124 | f"插件 '{plugin.get_plugin_name()}' 停止了消息链的后续处理。" 125 | ) 126 | break 127 | except Exception as e: 128 | self.logger.error( 129 | f"插件 '{plugin.get_plugin_name()}' 处理消息时出错: {e}", 130 | exc_info=True, 131 | ) 132 | excute_context.add_error(plugin.get_plugin_name(), str(e)) 133 | self.logger.info(f"插件处理消息完成") 134 | return excute_context.get_responses() 135 | 136 | def reload_all_plugins(self): 137 | """ 138 | 重新加载所有插件。 139 | 清空当前插件实例列表并重新发现、加载。 140 | """ 141 | self.logger.info("开始重新加载所有插件...") 142 | self.plugins.clear() 143 | self.load_plugins() 144 | self.logger.info("插件热重载完成。") 145 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/plugins/task.md: -------------------------------------------------------------------------------- 1 | omni-bot-sdk 插件协议同步维护说明 2 | 任务目标 3 | 实现插件开发的解耦与类型安全:通过 omni_bot_sdk/plugins/interface.py 统一导出所有插件开发所需的基类、协议(Protocol)、服务类型和常用类型,插件开发者只需从该文件导入即可获得完整类型提示和文档说明。 4 | 自动化、结构化注释:所有协议类、属性、方法均补充了 Google 风格的 docstring,详细说明参数(Args)、返回值(Returns)及字段含义,极大提升开发体验和可维护性。 5 | 主要内容 6 | 1. 协议分类与导出 7 | 插件基类:Plugin 8 | Bot协议:Bot(主Bot对象,聚合所有服务和生命周期方法) 9 | Service协议:如 UserService、DatabaseService、ImageProcessor、OCRProcessor、WindowManager、RPAController、PluginManager、MessageService、RPAService、MQTTService、ProcessorService 等 10 | RPAAction协议:所有RPA相关Action类型及 RPAActionType、RPAAction 11 | 消息类型协议:MessageType 12 | 插件上下文协议:PluginExcuteContext 13 | 用户信息协议:UserInfo,已补全所有字段和注释 14 | 2. 注释与类型提示规范 15 | 每个协议类均有简明 docstring,说明用途和典型用法 16 | 每个属性/方法均有中文注释 17 | 每个方法均有 Google 风格的 Args/Returns 说明,参数类型、含义、返回值一目了然 18 | 所有类型提示均与实际实现严格对应 19 | 后续维护与同步流程 20 | > 当你修改了某个服务、协议或数据类(如 UserService、UserInfo、DatabaseService 等)时,请按照以下流程同步更新 interface.py,以保证类型提示和注释始终与实现一致。 21 | 步骤 22 | 定位变更点 23 | 明确你修改了哪个服务/类/协议(如新增字段、方法、参数、类型变更等) 24 | 同步到 interface.py 25 | 在 omni_bot_sdk/plugins/interface.py 中找到对应的 Protocol 协议 26 | 补充/修改属性、方法签名,确保与实现完全一致 27 | 补充/修改 docstring,详细说明新增/变更的参数、返回值、用途 28 | 保持注释风格与 Google 风格一致 29 | 检查 _all_ 导出 30 | 如有新增协议类型,记得补充到 __all__ 列表 31 | 插件开发者同步 32 | 通知插件开发者只需从 interface.py 导入协议类型,无需关心 SDK 内部结构 33 | 可选:自动化校验 34 | 推荐后续可开发脚本自动比对实现与 interface.py 的一致性,减少人工遗漏 35 | 示例(同步 UserInfo 字段) 36 | 假设你在 models.py 中为 UserInfo 新增了 email: str 字段: 37 | 在 interface.py 的 UserInfo(Protocol) 中同步新增 38 | Apply to interface.py 39 | 在 docstring 中补充说明 40 | Apply to interface.py 41 | 维护建议 42 | 每次服务/协议/数据类有变更,务必同步更新 interface.py 43 | 保持注释和类型提示的准确性、完整性 44 | 如有大规模重构,建议全量比对实现与协议定义 45 | 46 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/rpa/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | RPA 包初始化文件。 3 | 本包包含 RPA 自动化相关的核心组件与工具类,统一对外导出主要接口。 4 | """ 5 | 6 | from omni_bot_sdk.weixin.message_classes import MessageType 7 | from .controller import RPAController 8 | from .image_processor import ImageProcessor 9 | from .input_handler import InputHandler 10 | from .message_sender import MessageSender 11 | from .ocr_processor import OCRProcessor 12 | from .window_manager import WindowManager 13 | from .ui_helper import UIInteractionHelper 14 | 15 | __all__ = [ 16 | "WindowManager", 17 | "MessageSender", 18 | "ImageProcessor", 19 | "OCRProcessor", 20 | "InputHandler", 21 | "RPAController", 22 | "MessageType", 23 | "UIInteractionHelper", 24 | ] 25 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/rpa/action_handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from .base_handler import RPAActionType, RPAAction, BaseActionHandler 2 | from .announcement_handler import ( 3 | PublicRoomAnnouncementHandler, 4 | PublicRoomAnnouncementAction, 5 | ) 6 | from .download_file_handler import DownloadFileHandler, DownloadFileAction 7 | from .download_image_handler import DownloadImageHandler, DownloadImageAction 8 | from .download_video_handler import DownloadVideoHandler, DownloadVideoAction 9 | from .forward_message_handler import ForwardMessageHandler, ForwardMessageAction 10 | from .leave_room_handler import LeaveRoomHandler, LeaveRoomAction 11 | from .pat_handler import PatHandler, PatAction 12 | from .remove_room_member_handler import RemoveRoomMemberHandler, RemoveRoomMemberAction 13 | from .rename_name_in_room_handler import RenameNameInRoomHandler, RenameNameInRoomAction 14 | from .rename_room_name_handler import RenameRoomNameHandler, RenameRoomNameAction 15 | from .rename_room_remark_handler import RenameRoomRemarkHandler, RenameRoomRemarkAction 16 | from .send_file_handler import SendFileHandler, SendFileAction 17 | from .send_image_handler import SendImageHandler, SendImageAction 18 | from .send_text_message_handler import SendTextMessageHandler, SendTextMessageAction 19 | from .switch_conversation_handler import ( 20 | SwitchConversationHandler, 21 | SwitchConversationAction, 22 | ) 23 | 24 | try: 25 | from .pro import * 26 | except ImportError: 27 | from .functional import * 28 | 29 | __all__ = [ 30 | "PublicRoomAnnouncementHandler", 31 | "PublicRoomAnnouncementAction", 32 | "DownloadFileHandler", 33 | "DownloadFileAction", 34 | "DownloadImageHandler", 35 | "DownloadImageAction", 36 | "DownloadVideoHandler", 37 | "DownloadVideoAction", 38 | "ForwardMessageHandler", 39 | "ForwardMessageAction", 40 | "LeaveRoomHandler", 41 | "LeaveRoomAction", 42 | "PatHandler", 43 | "PatAction", 44 | "RemoveRoomMemberHandler", 45 | "RemoveRoomMemberAction", 46 | "RenameNameInRoomHandler", 47 | "RenameNameInRoomAction", 48 | "RenameRoomNameHandler", 49 | "RenameRoomNameAction", 50 | "RenameRoomRemarkHandler", 51 | "RenameRoomRemarkAction", 52 | "SendFileHandler", 53 | "SendFileAction", 54 | "SendImageHandler", 55 | "SendImageAction", 56 | "SendTextMessageHandler", 57 | "SendTextMessageAction", 58 | "SwitchConversationHandler", 59 | "SwitchConversationAction", 60 | "Invite2RoomHandler", 61 | "Invite2RoomAction", 62 | "NewFriendHandler", 63 | "NewFriendAction", 64 | "SendPyqHandler", 65 | "SendPyqAction", 66 | "RPAActionType", 67 | "RPAAction", 68 | "BaseActionHandler", 69 | ] 70 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/rpa/action_handlers/base_handler.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from dataclasses import dataclass, field 3 | from datetime import datetime 4 | from enum import Enum 5 | from typing import TYPE_CHECKING, Any, Dict 6 | 7 | if TYPE_CHECKING: 8 | from omni_bot_sdk.rpa.controller import RPAController 9 | from omni_bot_sdk.rpa.image_processor import ImageProcessor 10 | from omni_bot_sdk.rpa.input_handler import InputHandler 11 | from omni_bot_sdk.rpa.ocr_processor import OCRProcessor 12 | from omni_bot_sdk.rpa.ui_helper import UIInteractionHelper 13 | from omni_bot_sdk.rpa.window_manager import WindowManager 14 | 15 | 16 | class RPAActionType(Enum): 17 | """RPA操作类型枚举""" 18 | 19 | SEND_MESSAGE = "send_message" 20 | SWITCH_CONVERSATION = "switch_conversation" 21 | QUOTE_MESSAGE = "quote_message" 22 | MENTION_USER = "mention_user" 23 | SEND_IMAGE = "send_image" 24 | # 总觉得全部统一使用发送文件就可以了,图片,视频都是一样的 25 | SEND_FILE = "send_file" 26 | SEND_EMOJI = "send_emoji" 27 | SEND_LINK = "send_link" 28 | SEND_VIDEO = "send_video" 29 | SEND_VOICE = "send_voice" 30 | FORWARD_MESSAGE = "forward_message" 31 | DOWNLOAD_IMAGE = "download_image" 32 | DOWNLOAD_VIDEO = "download_video" 33 | DOWNLOAD_FILE = "download_file" 34 | PAT = "pat" 35 | NEW_FRIEND = "new_friend" 36 | INVITE_2_ROOM = "invice_2_room" 37 | REMOVE_ROOM_MEMBER = "remove_room_member" 38 | SEND_PYQ = "send_pyq" 39 | PUBLIC_ROOM_ANNOUNCEMENT = "public_room_announcement" 40 | RENAME_ROOM_NAME = "rename_room_name" 41 | RENAME_ROOM_REMARK = "rename_room_remark" 42 | RENAME_NAME_IN_ROOM = "rename_name_in_room" 43 | LEAVE_ROOM = "leave_room" 44 | SEND_TEXT_MESSAGE = "send_text_message" 45 | 46 | 47 | @dataclass 48 | class RPAAction: 49 | """ 50 | RPA操作的基类。 51 | 52 | Attributes: 53 | action_type (RPAActionType): 操作类型。 54 | timestamp (datetime): 操作时间戳。 55 | """ 56 | 57 | action_type: RPAActionType = field(default=None, init=False) 58 | timestamp: datetime = field(default_factory=datetime.now, init=False) 59 | is_send_message: bool = field(default=None, init=False) 60 | 61 | def to_dict(self) -> Dict[str, Any]: 62 | """将RPAAction对象转换为字典。""" 63 | # 注意这里的 self.action_type 可能还未被 __post_init__ 设置 64 | # 一个更健壮的实现 65 | action_type_value = getattr(self, "action_type", None) 66 | if action_type_value is None: 67 | raise NotImplementedError( 68 | f"{self.__class__.__name__} is missing action_type." 69 | ) 70 | 71 | return { 72 | "action_type": action_type_value.value, 73 | "timestamp": self.timestamp.isoformat(), 74 | } 75 | 76 | 77 | class BaseActionHandler(ABC): 78 | """ 79 | RPA操作处理器基类,定义所有ActionHandler的接口和通用逻辑。 80 | """ 81 | 82 | def __init__(self, controller: "RPAController"): 83 | """ 84 | 初始化 BaseActionHandler。 85 | 86 | Args: 87 | controller (Any): RPAController实例。 88 | """ 89 | self.controller: "RPAController" = controller 90 | self.window_manager: "WindowManager" = controller.window_manager 91 | self.image_processor: "ImageProcessor" = controller.image_processor 92 | self.ocr_processor: "OCRProcessor" = controller.ocr_processor 93 | self.input_handler: "InputHandler" = controller.input_handler 94 | self.ui_helper: "UIInteractionHelper" = controller.ui_helper 95 | self.logger = controller.logger.getChild(self.__class__.__name__) 96 | 97 | @abstractmethod 98 | def execute(self, action: Any) -> bool: 99 | """ 100 | 执行具体的RPA操作。 101 | 102 | Args: 103 | action (Any): RPAAction实例。 104 | 105 | Returns: 106 | bool: 操作是否成功。 107 | """ 108 | pass 109 | 110 | def _cleanup(self): 111 | """ 112 | 公共清理逻辑:关闭所有弹窗和侧边栏。 113 | """ 114 | try: 115 | self.window_manager.close_all_windows() 116 | self.window_manager.open_close_sidebar(close=True) 117 | except Exception as e: 118 | self.logger.warning(f"清理时发生异常: {e}") 119 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/rpa/action_handlers/download_file_handler.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import Optional 3 | 4 | from omni_bot_sdk.rpa.action_handlers.base_handler import ( 5 | BaseActionHandler, 6 | RPAAction, 7 | RPAActionType, 8 | ) 9 | 10 | 11 | @dataclass 12 | class DownloadFileAction(RPAAction): 13 | file_url: Optional[str] = None 14 | save_path: Optional[str] = None 15 | 16 | def __post_init__(self): 17 | self.action_type = RPAActionType.DOWNLOAD_FILE 18 | self.is_send_message = False 19 | 20 | 21 | class DownloadFileHandler(BaseActionHandler): 22 | """ 23 | 下载文件操作的处理器。 24 | """ 25 | 26 | def execute(self, action: DownloadFileAction) -> bool: 27 | """ 28 | 执行下载文件操作。 29 | Args: 30 | action (DownloadFileAction): 操作对象。 31 | Returns: 32 | bool: 操作是否成功。 33 | """ 34 | try: 35 | self.logger.info(f"下载文件: {action.to_dict()}") 36 | # TODO: 实现具体的下载文件逻辑,这里能拿到文件的地址应该,可以转发出去,下载功能先不做了 37 | return True 38 | finally: 39 | self._cleanup() 40 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/rpa/action_handlers/download_image_handler.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import Optional 3 | 4 | import pyautogui 5 | from omni_bot_sdk.rpa.action_handlers.base_handler import ( 6 | BaseActionHandler, 7 | RPAAction, 8 | RPAActionType, 9 | ) 10 | 11 | 12 | @dataclass 13 | class DownloadImageAction(RPAAction): 14 | target: Optional[str] = None 15 | max_count: int = 1 16 | 17 | def __post_init__(self): 18 | self.action_type = RPAActionType.DOWNLOAD_IMAGE 19 | self.is_send_message = False 20 | 21 | 22 | class DownloadImageHandler(BaseActionHandler): 23 | """ 24 | 下载图片操作的处理器。 25 | """ 26 | 27 | def execute(self, action: DownloadImageAction) -> bool: 28 | """ 29 | 执行下载图片操作。 30 | Args: 31 | action (DownloadImageAction): 操作对象。 32 | Returns: 33 | bool: 操作是否成功。 34 | """ 35 | try: 36 | if not self.window_manager.switch_session(action.target): 37 | return False 38 | pyautogui.scroll(-1000) 39 | # TODO: 实现高清图片下载逻辑 40 | # 目前仅切换会话并返回True,后续可补充保存图片等功能 41 | return True 42 | finally: 43 | self._cleanup() 44 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/rpa/action_handlers/download_video_handler.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | import time 3 | from typing import Optional 4 | 5 | import pyautogui 6 | from omni_bot_sdk.rpa.action_handlers.base_handler import ( 7 | BaseActionHandler, 8 | RPAAction, 9 | RPAActionType, 10 | ) 11 | from omni_bot_sdk.utils.helpers import get_center_point 12 | from omni_bot_sdk.utils.mouse import human_like_mouse_move 13 | 14 | 15 | @dataclass 16 | class DownloadVideoAction(RPAAction): 17 | target: Optional[str] = None 18 | name: Optional[str] = None 19 | max_count: int = 1 20 | is_chatroom: bool = False 21 | 22 | def __post_init__(self): 23 | self.action_type = RPAActionType.DOWNLOAD_VIDEO 24 | self.is_send_message = False 25 | 26 | 27 | class DownloadVideoHandler(BaseActionHandler): 28 | """ 29 | 下载视频操作的处理器。 30 | """ 31 | 32 | def execute(self, action: DownloadVideoAction) -> bool: 33 | """ 34 | 执行下载视频操作。 35 | Args: 36 | action (DownloadVideoAction): 操作对象。 37 | Returns: 38 | bool: 操作是否成功。 39 | """ 40 | try: 41 | if not self.window_manager.switch_session(action.target): 42 | return False 43 | # 识别视频, 拿到会话区域,对这个区域进行yolo 44 | time.sleep(2) 45 | left, top, w, h = self.window_manager.get_message_region() 46 | screenshot = self.image_processor.take_screenshot(region=[left, top, w, h]) 47 | detections = self.image_processor.detect_objects(screenshot) 48 | detections_video = [d for d in detections if d.get("label") == "video"] 49 | if len(detections_video) == 0: 50 | self.logger.warn("没有找到视频消息") 51 | return False 52 | # 按照Y从大到小排列 53 | detections_video.sort(key=lambda x: x.get("pixel_bbox")[1], reverse=True) 54 | for video in detections_video: 55 | bbox = video.get("pixel_bbox") 56 | center = get_center_point(bbox) 57 | human_like_mouse_move( 58 | target_x=center[0] + left, 59 | target_y=center[1] + top, 60 | ) 61 | pyautogui.click() 62 | # TODO: 消息刷新的情况下,视频位置可能会变化,看yolo识别的速度了 63 | self.image_processor.draw_boxes_on_screen( 64 | screenshot, 65 | detections, 66 | "runtime_images/download_video.png", 67 | ) 68 | return True 69 | finally: 70 | self._cleanup() 71 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/rpa/action_handlers/forward_message_handler.py: -------------------------------------------------------------------------------- 1 | import time 2 | from dataclasses import dataclass, field 3 | 4 | import pyautogui 5 | from omni_bot_sdk.rpa.action_handlers.base_handler import ( 6 | BaseActionHandler, 7 | RPAAction, 8 | RPAActionType, 9 | ) 10 | 11 | 12 | @dataclass 13 | class ForwardMessageAction(RPAAction): 14 | def __post_init__(self): 15 | self.action_type = RPAActionType.FORWARD_MESSAGE 16 | self.is_send_message = False 17 | 18 | 19 | class ForwardMessageHandler(BaseActionHandler): 20 | """ 21 | 转发消息操作的处理器。 22 | """ 23 | 24 | def execute(self, action: ForwardMessageAction) -> bool: 25 | """ 26 | 执行转发消息操作。 27 | Args: 28 | action (ForwardMessageAction): 操作对象。 29 | Returns: 30 | bool: 操作是否成功。 31 | """ 32 | try: 33 | self.logger.warn("未实现转发消息操作") 34 | return False 35 | finally: 36 | self._cleanup() 37 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/rpa/action_handlers/functional/__init__.py: -------------------------------------------------------------------------------- 1 | from .invite2room_handler import Invite2RoomHandler, Invite2RoomAction 2 | from .new_friend_handler import NewFriendHandler, NewFriendAction 3 | from .send_pyq_handler import SendPyqHandler, SendPyqAction 4 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/rpa/action_handlers/functional/invite2room_handler.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | 3 | from omni_bot_sdk.rpa.action_handlers.base_handler import ( 4 | BaseActionHandler, 5 | RPAAction, 6 | RPAActionType, 7 | ) 8 | 9 | 10 | @dataclass 11 | class Invite2RoomAction(RPAAction): 12 | """ 13 | 邀请加群操作。 14 | 15 | Attributes: 16 | user_name (str): 被邀请的用户名。 17 | target (str): 目标群聊的名称。 18 | """ 19 | 20 | user_name: str = field(default=None) 21 | target: str = field(default=None) 22 | 23 | def __post_init__(self): 24 | self.action_type = RPAActionType.INVITE_2_ROOM 25 | self.is_send_message = True 26 | 27 | 28 | class Invite2RoomHandler(BaseActionHandler): 29 | """邀请好友进群操作的处理器。""" 30 | 31 | def execute(self, action: Invite2RoomAction) -> bool: 32 | self.logger.warn("Invite2RoomHandler is not implemented") 33 | return False 34 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/rpa/action_handlers/functional/new_friend_handler.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | 4 | from omni_bot_sdk.rpa.action_handlers.base_handler import ( 5 | BaseActionHandler, 6 | RPAActionType, 7 | RPAAction, 8 | ) 9 | from omni_bot_sdk.rpa.action_handlers.mixins.window_operations_mixin import ( 10 | WindowOperationsMixin, 11 | ) 12 | 13 | 14 | @dataclass 15 | class NewFriendAction(RPAAction): 16 | """ 17 | 新好友操作。 18 | TODO 必须重新实现,目前方案不稳定 19 | Attributes: 20 | user_name (str): 新好友用户名。 21 | action (str): 操作类型(同意、拒绝、忽略)。 22 | response (str): 拒绝时的回复内容。 23 | """ 24 | 25 | user_name: Optional[str] = None 26 | action: Optional[str] = None 27 | response: Optional[str] = None 28 | index: Optional[int] = None 29 | 30 | def __post_init__(self): 31 | self.action_type = RPAActionType.NEW_FRIEND 32 | self.is_send_message = True 33 | 34 | 35 | class NewFriendHandler(WindowOperationsMixin, BaseActionHandler): 36 | """ 37 | 新好友操作的处理器。 38 | """ 39 | 40 | def execute(self, action: NewFriendAction) -> bool: 41 | self.logger.warn("NewFriendHandler is not implemented") 42 | return False 43 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/rpa/action_handlers/functional/send_pyq_handler.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import List 3 | 4 | from omni_bot_sdk.rpa.action_handlers.base_handler import ( 5 | BaseActionHandler, 6 | RPAAction, 7 | RPAActionType, 8 | ) 9 | 10 | 11 | @dataclass 12 | class SendPyqAction(RPAAction): 13 | """ 14 | 朋友圈发送操作的数据结构。 15 | """ 16 | 17 | images: List[str] = field(default_factory=list) 18 | content: str = "" 19 | 20 | def __post_init__(self): 21 | self.action_type = RPAActionType.SEND_PYQ 22 | self.is_send_message = True 23 | 24 | 25 | class SendPyqHandler(BaseActionHandler): 26 | def execute(self, action: SendPyqAction) -> bool: 27 | self.logger.warn("SendPyqHandler is not implemented") 28 | return False 29 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/rpa/action_handlers/leave_room_handler.py: -------------------------------------------------------------------------------- 1 | import time 2 | from dataclasses import dataclass, field 3 | 4 | import pyautogui 5 | from omni_bot_sdk.rpa.action_handlers.base_handler import ( 6 | BaseActionHandler, 7 | RPAAction, 8 | RPAActionType, 9 | ) 10 | from omni_bot_sdk.rpa.action_handlers.mixins.group_operations_mixin import ( 11 | GroupOperationsMixin, 12 | ) 13 | from omni_bot_sdk.rpa.action_handlers.mixins.window_operations_mixin import ( 14 | WindowOperationsMixin, 15 | ) 16 | from omni_bot_sdk.rpa.window_manager import WindowTypeEnum 17 | from omni_bot_sdk.utils.helpers import get_center_point 18 | from omni_bot_sdk.utils.mouse import human_like_mouse_move 19 | 20 | 21 | @dataclass 22 | class LeaveRoomAction(RPAAction): 23 | target: str = field(default=None) 24 | 25 | def __post_init__(self): 26 | self.action_type = RPAActionType.LEAVE_ROOM 27 | self.is_send_message = False 28 | 29 | 30 | class LeaveRoomHandler(WindowOperationsMixin, GroupOperationsMixin, BaseActionHandler): 31 | """ 32 | 退群操作的处理器。 33 | """ 34 | 35 | def execute(self, action: LeaveRoomAction) -> bool: 36 | """ 37 | 执行退群操作。 38 | Args: 39 | action (LeaveRoomAction): 操作对象。 40 | Returns: 41 | bool: 操作是否成功。 42 | """ 43 | try: 44 | if not self.window_manager.switch_session(action.target): 45 | self._cleanup() 46 | return False 47 | self.window_manager.open_close_sidebar() 48 | region = self._get_room_side_bar_region() 49 | human_like_mouse_move( 50 | region[0] + region[2] // 2, region[1] + region[3] // 2 51 | ) 52 | time.sleep(self.controller.window_manager.action_delay) 53 | pyautogui.scroll(-1500) 54 | leave_btn = self.ui_helper.find_and_click_text_element( 55 | region=region, 56 | text="退出群聊", 57 | ) 58 | if leave_btn: 59 | confirm_window = self.window_manager.wait_for_window( 60 | WindowTypeEnum.RoomInputConfirmBox 61 | ) 62 | if confirm_window: 63 | region = self.get_window_region(confirm_window) 64 | confirm_btn = self.ui_helper.find_and_click_text_element( 65 | region=region, 66 | text="确定", 67 | ) 68 | if confirm_btn: 69 | time.sleep(self.controller.window_manager.action_delay) 70 | else: 71 | self.logger.error("未找到确定按钮") 72 | return False 73 | else: 74 | self.logger.error("未找到确认窗口") 75 | return False 76 | else: 77 | self.logger.error("未找到退出群聊按钮") 78 | return False 79 | return True 80 | finally: 81 | self._cleanup() 82 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/rpa/action_handlers/mixins/group_operations_mixin.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Tuple 3 | 4 | import pyautogui 5 | from omni_bot_sdk.utils.helpers import set_clipboard_text 6 | 7 | 8 | class GroupOperationsMixin: 9 | 10 | def _get_room_side_bar_region(self) -> Tuple[int, int, int, int]: 11 | """ 12 | 获取群聊侧边栏区域。 13 | """ 14 | return [ 15 | self.window_manager.size_config.width 16 | - self.window_manager.ROOM_SIDE_BAR_WIDTH, 17 | self.window_manager.TITLE_BAR_HEIGHT, 18 | self.window_manager.ROOM_SIDE_BAR_WIDTH, 19 | self.window_manager.size_config.height 20 | - self.window_manager.TITLE_BAR_HEIGHT, 21 | ] 22 | 23 | def _replace_input_text(self, text: str) -> bool: 24 | """ 25 | 替换输入框文本。 26 | """ 27 | time.sleep(self.controller.window_manager.action_delay) 28 | set_clipboard_text(text) 29 | time.sleep(self.controller.window_manager.action_delay) 30 | pyautogui.hotkey("ctrl", "a") 31 | time.sleep(self.controller.window_manager.action_delay) 32 | pyautogui.hotkey("ctrl", "v") 33 | time.sleep(self.controller.window_manager.action_delay) 34 | pyautogui.press("enter") 35 | time.sleep(self.controller.window_manager.action_delay) 36 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/rpa/action_handlers/mixins/window_operations_mixin.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Tuple 3 | 4 | import pyautogui 5 | from omni_bot_sdk.rpa.window_manager import WindowTypeEnum 6 | from omni_bot_sdk.utils.helpers import get_center_point 7 | from omni_bot_sdk.utils.mouse import human_like_mouse_move 8 | 9 | 10 | class WindowOperationsMixin: 11 | 12 | def get_window_region(self, window: pyautogui.Window) -> Tuple[int, int, int, int]: 13 | """ 14 | 获取窗口区域。 15 | """ 16 | return [ 17 | window.left 18 | + self.controller.window_manager.rpa_config.get("window_margin", 20), 19 | window.top 20 | + self.controller.window_manager.rpa_config.get("window_margin", 20), 21 | window.width 22 | - self.controller.window_manager.rpa_config.get("window_margin", 20) * 2, 23 | window.height 24 | - self.controller.window_manager.rpa_config.get("window_margin", 20) * 2, 25 | ] 26 | 27 | def find_and_click_menu_item(self, menu_text: str) -> bool: 28 | """ 29 | 查找并点击菜单项 30 | 31 | Args: 32 | menu_text: 菜单项文本 33 | 34 | Returns: 35 | bool: 是否成功点击 36 | """ 37 | # 查找激活的会话窗口,判断条件:标题是 Weixin,大小不会很大(有点模糊的判断条件 TODO 等待优化) 38 | menu_window = self.controller.window_manager.wait_for_window( 39 | WindowTypeEnum.MenuWindow 40 | ) 41 | if not menu_window: 42 | return False 43 | try: 44 | region = self.get_window_region(menu_window) 45 | screenshot = self.controller.image_processor.take_screenshot( 46 | region=region, save_path="runtime_images/menu.png" 47 | ) 48 | # 使用OCR查找菜单项 49 | formatted_results = self.ocr_processor.process_image(image=screenshot) 50 | formatted_results = [ 51 | d for d in formatted_results if d.get("label") == menu_text 52 | ] 53 | if len(formatted_results) > 0: 54 | bbox = formatted_results[0].get("pixel_bbox") 55 | center = get_center_point(bbox) 56 | human_like_mouse_move( 57 | target_x=center[0] + region[0], 58 | target_y=center[1] + region[1], 59 | ) 60 | time.sleep(self.controller.window_manager.action_delay) 61 | pyautogui.click() 62 | return True 63 | return False 64 | 65 | except Exception as e: 66 | self.logger.error(f"查找并点击菜单项时出错: {str(e)}") 67 | return False 68 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/rpa/action_handlers/pat_handler.py: -------------------------------------------------------------------------------- 1 | import time 2 | from dataclasses import dataclass, field 3 | 4 | import pyautogui 5 | from omni_bot_sdk.rpa.action_handlers.base_handler import ( 6 | BaseActionHandler, 7 | RPAAction, 8 | RPAActionType, 9 | ) 10 | from omni_bot_sdk.rpa.action_handlers.mixins.window_operations_mixin import ( 11 | WindowOperationsMixin, 12 | ) 13 | from omni_bot_sdk.utils.helpers import get_center_point 14 | from omni_bot_sdk.utils.mouse import human_like_mouse_move 15 | 16 | 17 | @dataclass 18 | class PatAction(RPAAction): 19 | target: str = None 20 | user_name: str = None 21 | is_chatroom: bool = False 22 | 23 | def __post_init__(self): 24 | self.action_type = RPAActionType.PAT 25 | self.is_send_message = True 26 | 27 | 28 | class PatHandler(WindowOperationsMixin, BaseActionHandler): 29 | """ 30 | 拍一拍操作的处理器。 31 | """ 32 | 33 | def execute(self, action: PatAction) -> bool: 34 | try: 35 | if not self.window_manager.switch_session(action.target): 36 | return False 37 | region = self.window_manager.get_message_region() 38 | image = self.image_processor.take_screenshot(region=region) 39 | if action.is_chatroom: 40 | # TODO 用名字去找,误差可能性大,配合数据库+YOLO,可以拿到高准确率的结果 41 | positions = self.ocr_processor.find_text( 42 | image=image, target_text=action.user_name 43 | ) 44 | if len(positions) > 0: 45 | avatar_positions = [] 46 | parsed_result = self.image_processor.detect_objects(image=image) 47 | for result in parsed_result: 48 | if result.get("label") == "avatar": 49 | bbox = result.get("pixel_bbox") 50 | if bbox[0] / self.window_manager.MSG_WIDTH < 0.5: 51 | avatar_positions.append(result) 52 | self.image_processor.draw_boxes_on_screen( 53 | self.image_processor.take_screenshot(region=region), 54 | parsed_result, 55 | "runtime_images/pat.png", 56 | ) 57 | ava = None 58 | if len(avatar_positions) > 0: 59 | for name in positions: 60 | for avatar in avatar_positions: 61 | if ( 62 | abs( 63 | avatar.get("pixel_bbox")[1] 64 | - name.get("pixel_bbox")[1] 65 | ) 66 | < 10 67 | ): 68 | ava = avatar 69 | break 70 | if ava: 71 | break 72 | if ava: 73 | bbox = ava.get("pixel_bbox") 74 | center = get_center_point(bbox=bbox) 75 | human_like_mouse_move( 76 | center[0] + region[0], 77 | center[1] + region[1], 78 | ) 79 | pyautogui.rightClick() 80 | time.sleep(self.controller.window_manager.action_delay) 81 | else: 82 | self.logger.warn( 83 | f"找到的消息位置置信度太低,不进行拍一拍: {ava}" 84 | ) 85 | return False 86 | else: 87 | self.logger.warn("对话框都找不到消息啊") 88 | return False 89 | else: 90 | avatar_position = self.image_processor.detect_objects(image=image) 91 | find = False 92 | for result in avatar_position: 93 | if result.get("label") == "avatar": 94 | avatar_bbox = result.get("pixel_bbox") 95 | if ( 96 | avatar_bbox 97 | and avatar_bbox[0] / self.window_manager.MSG_WIDTH < 0.5 98 | ): 99 | center = get_center_point(avatar_bbox) 100 | human_like_mouse_move( 101 | center[0] + region[0], 102 | center[1] + region[1], 103 | ) 104 | pyautogui.rightClick() 105 | time.sleep(self.controller.window_manager.action_delay) 106 | find = True 107 | break 108 | if not find: 109 | self.logger.warn("私聊没有找到头像") 110 | return False 111 | self.image_processor.draw_boxes_on_screen( 112 | self.image_processor.take_screenshot(region=region), 113 | avatar_position, 114 | "runtime_images/pat.png", 115 | ) 116 | return self.find_and_click_menu_item("拍一拍") 117 | finally: 118 | self._cleanup() 119 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/rpa/action_handlers/remove_room_member_handler.py: -------------------------------------------------------------------------------- 1 | import time 2 | from dataclasses import dataclass, field 3 | 4 | import pyautogui 5 | from omni_bot_sdk.rpa.ui_helper import BtnType 6 | from omni_bot_sdk.rpa.window_manager import WindowTypeEnum 7 | from omni_bot_sdk.utils.helpers import set_clipboard_text 8 | from omni_bot_sdk.utils.mouse import human_like_mouse_move 9 | 10 | from omni_bot_sdk.rpa.action_handlers.base_handler import ( 11 | BaseActionHandler, 12 | RPAAction, 13 | RPAActionType, 14 | ) 15 | 16 | 17 | @dataclass 18 | class RemoveRoomMemberAction(RPAAction): 19 | """ 20 | 移除群成员操作。 21 | 22 | Attributes: 23 | user_name (str): 被移除的用户名。 24 | target (str): 目标群聊的名称。 25 | """ 26 | 27 | user_name: str = field(default=None) 28 | target: str = field(default=None) 29 | 30 | def __post_init__(self): 31 | self.action_type = RPAActionType.REMOVE_ROOM_MEMBER 32 | self.is_send_message = False 33 | 34 | 35 | class RemoveRoomMemberHandler(BaseActionHandler): 36 | """ 37 | 移除群成员操作的处理器。 38 | """ 39 | 40 | def execute(self, action: RemoveRoomMemberAction) -> bool: 41 | """ 42 | 执行移除群成员操作。 43 | """ 44 | try: 45 | if not self.window_manager.switch_session(action.target): 46 | self.logger.error(f"切换到群失败: {action.target}") 47 | return False 48 | self.window_manager.open_close_sidebar() 49 | if not self._click_remove_button(): 50 | self.logger.error("未找到移出按钮") 51 | return False 52 | popup_window = self.window_manager.wait_for_window( 53 | WindowTypeEnum.RemoveMemberWindow 54 | ) 55 | if not popup_window: 56 | self.logger.error("移出群成员窗口打开失败") 57 | return False 58 | if not self._select_and_confirm_remove(action, popup_window): 59 | self.logger.error("移除成员失败") 60 | return False 61 | return True 62 | finally: 63 | self._cleanup() 64 | 65 | def _click_remove_button(self) -> bool: 66 | """ 67 | 在侧边栏查找并点击"移出"按钮。 68 | """ 69 | region = [ 70 | self.window_manager.size_config.width 71 | - self.window_manager.ROOM_SIDE_BAR_WIDTH, 72 | self.window_manager.TITLE_BAR_HEIGHT, 73 | self.window_manager.ROOM_SIDE_BAR_WIDTH, 74 | self.window_manager.size_config.height // 2, 75 | ] 76 | btns = self.ui_helper.find_text_elements( 77 | region=region, 78 | text="移出", 79 | ) 80 | if btns: 81 | # 按照坐标排序,优先选Y最大,再选X最大 82 | btns.sort( 83 | key=lambda x: (x["pixel_bbox"][3], x["pixel_bbox"][0]), reverse=True 84 | ) 85 | self.ui_helper.click_element( 86 | bbox=btns[0].get("pixel_bbox"), 87 | offset=(0, -30), 88 | ) 89 | return True 90 | return False 91 | 92 | def _select_and_confirm_remove( 93 | self, action: RemoveRoomMemberAction, popup_window 94 | ) -> bool: 95 | """ 96 | 在弹窗中搜索并选择成员,点击确认移除。 97 | """ 98 | set_clipboard_text(action.user_name) 99 | time.sleep(self.controller.window_manager.action_delay) 100 | self.input_handler.hotkey("ctrl", "v") 101 | time.sleep(self.controller.window_manager.action_delay) 102 | region = [ 103 | popup_window.left + self.controller.window_manager.window_margin, 104 | popup_window.top + self.controller.window_manager.window_margin, 105 | popup_window.width // 2, 106 | popup_window.height // 2, 107 | ] 108 | # 这里就只截取了左上角1/4的区域进行识别,直接匹配名称,然后按照Y从大到小选择第一个? 109 | contacts = self.ui_helper.find_text_elements( 110 | action.user_name, region=region, fuzzy=70 111 | ) 112 | if contacts: 113 | contacts.sort(key=lambda x: x.get("pixel_bbox")[1], reverse=True) 114 | self.ui_helper.click_element( 115 | bbox=contacts[0].get("pixel_bbox"), 116 | ) 117 | 118 | # 在右下角1/4查找移出按钮 119 | region = [ 120 | popup_window.left + popup_window.width // 2, 121 | popup_window.top + popup_window.height // 2, 122 | popup_window.width // 2, 123 | popup_window.height // 2, 124 | ] 125 | btns = self.ui_helper.find_btn_by_text( 126 | text="", 127 | btn_type=BtnType.GREEN, 128 | region=region, 129 | ) 130 | if btns: 131 | self.ui_helper.click_element(bbox=btns) 132 | return True 133 | return True 134 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/rpa/action_handlers/rename_name_in_room_handler.py: -------------------------------------------------------------------------------- 1 | import time 2 | from dataclasses import dataclass, field 3 | 4 | import pyautogui 5 | from omni_bot_sdk.rpa.action_handlers.base_handler import ( 6 | BaseActionHandler, 7 | RPAAction, 8 | RPAActionType, 9 | ) 10 | from omni_bot_sdk.rpa.action_handlers.mixins.group_operations_mixin import ( 11 | GroupOperationsMixin, 12 | ) 13 | from omni_bot_sdk.rpa.action_handlers.mixins.window_operations_mixin import ( 14 | WindowOperationsMixin, 15 | ) 16 | from omni_bot_sdk.rpa.ui_helper import BtnType 17 | from omni_bot_sdk.rpa.window_manager import WindowTypeEnum 18 | from omni_bot_sdk.utils.helpers import get_center_point, set_clipboard_text 19 | from omni_bot_sdk.utils.mouse import human_like_mouse_move 20 | 21 | 22 | @dataclass 23 | class RenameNameInRoomAction(RPAAction): 24 | target: str = "" 25 | name: str = "" 26 | 27 | def __post_init__(self): 28 | self.action_type = RPAActionType.RENAME_NAME_IN_ROOM 29 | self.is_send_message = False 30 | 31 | 32 | class RenameNameInRoomHandler( 33 | WindowOperationsMixin, GroupOperationsMixin, BaseActionHandler 34 | ): 35 | """ 36 | 重命名自己在群昵称操作的处理器。 37 | """ 38 | 39 | def execute(self, action: RenameNameInRoomAction) -> bool: 40 | """ 41 | 执行重命名群聊操作。 42 | Args: 43 | action (RenameRoomNameAction): 操作对象。 44 | Returns: 45 | bool: 操作是否成功。 46 | """ 47 | try: 48 | if not self.window_manager.switch_session(action.target): 49 | self.logger.error(f"切换到群失败: {action.target}") 50 | return False 51 | self.window_manager.open_close_sidebar() 52 | # 截图侧边栏,查找群聊名称 53 | region = self._get_room_side_bar_region() 54 | room_name_elements = self.ui_helper.find_text_elements( 55 | text="我在本群的昵称", region=region, fuzzy=80 56 | ) 57 | if len(room_name_elements) == 1: 58 | name_bbox = room_name_elements[0].get("pixel_bbox") 59 | center = get_center_point(bbox=name_bbox) 60 | # 高度两倍偏移量 61 | name_input_y = (name_bbox[3] - name_bbox[1]) * 1.5 + center[1] 62 | human_like_mouse_move(center[0], name_input_y) 63 | pyautogui.click() 64 | time.sleep(self.controller.window_manager.action_delay) 65 | self._replace_input_text(action.name) 66 | # 查找确认弹窗 67 | confirm_window = self.window_manager.wait_for_window( 68 | WindowTypeEnum.RoomInputConfirmBox, timeout=10 69 | ) 70 | if confirm_window: 71 | region = self.get_window_region(confirm_window) 72 | confirm_btn = self.ui_helper.find_btn_by_text( 73 | text="", region=region, btn_type=BtnType.GREEN 74 | ) 75 | if confirm_btn: 76 | center = get_center_point(confirm_btn) 77 | human_like_mouse_move(center[0], center[1]) 78 | pyautogui.click() 79 | time.sleep(self.controller.window_manager.action_delay) 80 | return True 81 | else: 82 | self.logger.error("没有找到确认按钮") 83 | return False 84 | else: 85 | self.logger.error("没有找到确认窗口") 86 | return False 87 | else: 88 | self.logger.error("没有找到我的群昵称") 89 | return False 90 | finally: 91 | self._cleanup() 92 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/rpa/action_handlers/rename_room_name_handler.py: -------------------------------------------------------------------------------- 1 | import time 2 | from dataclasses import dataclass, field 3 | from datetime import datetime 4 | 5 | import pyautogui 6 | from omni_bot_sdk.rpa.action_handlers.base_handler import ( 7 | BaseActionHandler, 8 | RPAAction, 9 | RPAActionType, 10 | ) 11 | from omni_bot_sdk.rpa.action_handlers.mixins.group_operations_mixin import ( 12 | GroupOperationsMixin, 13 | ) 14 | from omni_bot_sdk.rpa.action_handlers.mixins.window_operations_mixin import ( 15 | WindowOperationsMixin, 16 | ) 17 | from omni_bot_sdk.rpa.ui_helper import BtnType 18 | from omni_bot_sdk.rpa.window_manager import WindowTypeEnum 19 | from omni_bot_sdk.utils.helpers import get_center_point, set_clipboard_text 20 | from omni_bot_sdk.utils.mouse import human_like_mouse_move 21 | 22 | 23 | @dataclass 24 | class RenameRoomNameAction(RPAAction): 25 | """ 26 | 重命名群聊操作的数据结构。 27 | """ 28 | 29 | target: str = "" 30 | name: str = "" 31 | 32 | def __post_init__(self): 33 | self.action_type = RPAActionType.RENAME_ROOM_NAME 34 | self.is_send_message = False 35 | 36 | 37 | class RenameRoomNameHandler( 38 | WindowOperationsMixin, GroupOperationsMixin, BaseActionHandler 39 | ): 40 | """ 41 | 重命名群聊操作的处理器。 42 | """ 43 | 44 | def execute(self, action: RenameRoomNameAction) -> bool: 45 | """ 46 | 执行重命名群聊操作。 47 | Args: 48 | action (RenameRoomNameAction): 操作对象。 49 | Returns: 50 | bool: 操作是否成功。 51 | """ 52 | try: 53 | if not self.window_manager.switch_session(action.target): 54 | self.logger.error(f"切换到群失败: {action.target}") 55 | return False 56 | self.window_manager.open_close_sidebar() 57 | # 截图侧边栏,查找群聊名称 58 | region = self._get_room_side_bar_region() 59 | room_name_elements = self.ui_helper.find_text_elements( 60 | text="群聊名称", region=region, fuzzy=100 61 | ) 62 | if len(room_name_elements) == 1: 63 | name_bbox = room_name_elements[0].get("pixel_bbox") 64 | center = get_center_point(bbox=name_bbox) 65 | # 高度两倍偏移量 66 | name_input_y = (name_bbox[3] - name_bbox[1]) * 1.5 + center[1] 67 | human_like_mouse_move(center[0], name_input_y) 68 | pyautogui.click() 69 | self._replace_input_text(action.name) 70 | # 查找确认弹窗 71 | confirm_window = self.window_manager.wait_for_window( 72 | WindowTypeEnum.RoomInputConfirmBox, timeout=10 73 | ) 74 | if confirm_window: 75 | region = self.get_window_region(confirm_window) 76 | confirm_btn = self.ui_helper.find_btn_by_text( 77 | text="", region=region, btn_type=BtnType.GREEN 78 | ) 79 | if confirm_btn: 80 | center = get_center_point(confirm_btn) 81 | human_like_mouse_move(center[0], center[1]) 82 | pyautogui.click() 83 | time.sleep(self.controller.window_manager.action_delay) 84 | return True 85 | else: 86 | self.logger.error("没有找到确认按钮") 87 | return False 88 | else: 89 | self.logger.error("没有找到确认窗口") 90 | return False 91 | else: 92 | self.logger.error("没有找到群昵称") 93 | return False 94 | finally: 95 | self._cleanup() 96 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/rpa/action_handlers/rename_room_remark_handler.py: -------------------------------------------------------------------------------- 1 | import time 2 | from dataclasses import dataclass, field 3 | 4 | import pyautogui 5 | from omni_bot_sdk.rpa.action_handlers.base_handler import ( 6 | BaseActionHandler, 7 | RPAAction, 8 | RPAActionType, 9 | ) 10 | from omni_bot_sdk.rpa.action_handlers.mixins.group_operations_mixin import ( 11 | GroupOperationsMixin, 12 | ) 13 | from omni_bot_sdk.rpa.window_manager import WindowTypeEnum 14 | from omni_bot_sdk.utils.helpers import get_center_point, set_clipboard_text 15 | from omni_bot_sdk.utils.mouse import human_like_mouse_move 16 | 17 | 18 | @dataclass 19 | class RenameRoomRemarkAction(RPAAction): 20 | target: str = "" 21 | remark: str = "" 22 | 23 | def __post_init__(self): 24 | self.action_type = RPAActionType.RENAME_ROOM_REMARK 25 | self.is_send_message = False 26 | 27 | 28 | class RenameRoomRemarkHandler(GroupOperationsMixin, BaseActionHandler): 29 | """ 30 | 重命名群聊备注操作的处理器。 31 | """ 32 | 33 | def execute(self, action: RenameRoomRemarkAction) -> bool: 34 | """ 35 | 执行重命名群聊备注操作。 36 | Args: 37 | action (RenameRoomRemarkAction): 操作对象。 38 | Returns: 39 | bool: 操作是否成功。 40 | """ 41 | try: 42 | if not self.window_manager.switch_session(action.target): 43 | self.logger.error(f"切换到群失败: {action.target}") 44 | return False 45 | self.window_manager.open_close_sidebar() 46 | region = self._get_room_side_bar_region() 47 | room_name_elements = self.ui_helper.find_text_elements( 48 | text="备注", region=region, fuzzy=100 49 | ) 50 | if len(room_name_elements) == 1: 51 | name_bbox = room_name_elements[0].get("pixel_bbox") 52 | center = get_center_point(bbox=name_bbox) 53 | # 高度两倍偏移量 54 | name_input_y = (name_bbox[3] - name_bbox[1]) * 1.5 + center[1] 55 | human_like_mouse_move(center[0], name_input_y) 56 | pyautogui.click() 57 | self._replace_input_text(action.remark) 58 | return True 59 | 60 | else: 61 | self.logger.error("没有找到群备注") 62 | return False 63 | finally: 64 | self._cleanup() 65 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/rpa/action_handlers/send_file_handler.py: -------------------------------------------------------------------------------- 1 | import time 2 | from dataclasses import dataclass, field 3 | from datetime import datetime 4 | from typing import Any, Dict, Optional 5 | 6 | import pyautogui 7 | from omni_bot_sdk.rpa.action_handlers.base_handler import ( 8 | BaseActionHandler, 9 | RPAAction, 10 | RPAActionType, 11 | ) 12 | from omni_bot_sdk.utils.helpers import copy_file_to_clipboard 13 | 14 | 15 | @dataclass 16 | class SendFileAction(RPAAction): 17 | """ 18 | 发送文件操作。 19 | Attributes: 20 | file_path (str): 文件路径。 21 | target (str): 目标用户或群聊的标识。 22 | is_chatroom (bool): 是否为群聊。 23 | """ 24 | 25 | file_path: Optional[str] = None 26 | target: Optional[str] = None 27 | is_chatroom: bool = False 28 | 29 | def __post_init__(self): 30 | self.action_type = RPAActionType.SEND_FILE 31 | self.is_send_message = True 32 | 33 | 34 | class SendFileHandler(BaseActionHandler): 35 | """ 36 | 发送文件操作的处理器。 37 | """ 38 | 39 | def execute(self, action: SendFileAction) -> bool: 40 | """ 41 | 执行发送文件操作。 42 | Args: 43 | action (SendFileAction): 操作对象。 44 | Returns: 45 | bool: 操作是否成功。 46 | """ 47 | try: 48 | if not self.window_manager.switch_session(action.target): 49 | return False 50 | if not copy_file_to_clipboard(action.file_path): 51 | return False 52 | time.sleep(self.controller.window_manager.action_delay) 53 | self.window_manager.activate_input_box() 54 | time.sleep(self.controller.window_manager.action_delay) 55 | pyautogui.hotkey("ctrl", "v") 56 | time.sleep(1) 57 | return self.ui_helper.click_send_button() 58 | finally: 59 | self._cleanup() 60 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/rpa/action_handlers/send_image_handler.py: -------------------------------------------------------------------------------- 1 | import time 2 | from dataclasses import dataclass, field 3 | from typing import Any, Dict, Optional 4 | 5 | import pyautogui 6 | from omni_bot_sdk.rpa.action_handlers.base_handler import ( 7 | BaseActionHandler, 8 | RPAAction, 9 | RPAActionType, 10 | ) 11 | from omni_bot_sdk.utils.helpers import copy_file_to_clipboard 12 | 13 | 14 | @dataclass 15 | class SendImageAction(RPAAction): 16 | """ 17 | 发送图片操作。 18 | Attributes: 19 | image_path (str): 图片文件路径。 20 | target (str): 目标用户或群聊的标识。 21 | is_chatroom (bool): 是否为群聊。 22 | """ 23 | 24 | image_path: Optional[str] = None 25 | target: Optional[str] = None 26 | is_chatroom: bool = False 27 | 28 | def __post_init__(self): 29 | self.action_type = RPAActionType.SEND_IMAGE 30 | self.is_send_message = True 31 | 32 | 33 | class SendImageHandler(BaseActionHandler): 34 | """ 35 | 发送图片操作的处理器。 36 | """ 37 | 38 | def execute(self, action: SendImageAction) -> bool: 39 | """ 40 | 执行发送图片操作。 41 | Args: 42 | action (SendImageAction): 操作对象。 43 | Returns: 44 | bool: 操作是否成功。 45 | """ 46 | try: 47 | if not self.window_manager.switch_session(action.target): 48 | return False 49 | if not copy_file_to_clipboard(action.image_path): 50 | return False 51 | time.sleep(self.controller.window_manager.action_delay) 52 | self.window_manager.activate_input_box() 53 | time.sleep(self.controller.window_manager.action_delay) 54 | pyautogui.hotkey("ctrl", "v") 55 | time.sleep(1) 56 | return self.ui_helper.click_send_button() 57 | finally: 58 | self._cleanup() 59 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/rpa/action_handlers/send_text_message_handler.py: -------------------------------------------------------------------------------- 1 | import time 2 | from dataclasses import dataclass, field 3 | from datetime import datetime 4 | from typing import Any, Dict, Optional 5 | 6 | from omni_bot_sdk.rpa.action_handlers.base_handler import ( 7 | BaseActionHandler, 8 | RPAAction, 9 | RPAActionType, 10 | ) 11 | from omni_bot_sdk.utils.helpers import set_clipboard_text 12 | 13 | 14 | @dataclass 15 | class SendTextMessageAction(RPAAction): 16 | """ 17 | 发送消息操作。 18 | 19 | Attributes: 20 | content (str): 要发送的消息内容。 21 | target (str): 目标用户或群聊的标识。 22 | is_chatroom (bool): 是否为群聊。 23 | at_user_name (str): 需要@的用户名(仅群聊有效)。 24 | quote_message (str): 需要引用的消息内容。 25 | random_at_quote (bool): 是否随机@或引用。 26 | """ 27 | 28 | content: str = field(default=None) 29 | target: str = field(default=None) 30 | is_chatroom: bool = field(default=False) 31 | at_user_name: str = field(default=None) 32 | quote_message: str = field(default=None) 33 | random_at_quote: bool = field(default=False) 34 | 35 | def __post_init__(self): 36 | self.action_type = RPAActionType.SEND_TEXT_MESSAGE 37 | self.is_send_message = True 38 | 39 | 40 | class SendTextMessageHandler(BaseActionHandler): 41 | """ 42 | 发送消息操作的处理器。 43 | """ 44 | 45 | def execute(self, action: SendTextMessageAction) -> bool: 46 | """ 47 | 执行发送消息操作。 48 | Args: 49 | action (SendTextMessageAction): 操作对象。 50 | Returns: 51 | bool: 操作是否成功。 52 | """ 53 | try: 54 | if not self.window_manager.switch_session(action.target): 55 | return False 56 | if action.at_user_name: 57 | self.controller.message_sender.clear_input_box() 58 | self.controller.message_sender.mention_user(action.at_user_name) 59 | time.sleep(self.controller.window_manager.action_delay) 60 | return self.controller.message_sender.send_message( 61 | action.content, action.at_user_name is None 62 | ) 63 | finally: 64 | self._cleanup() 65 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/rpa/action_handlers/switch_conversation_handler.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from datetime import datetime 3 | from typing import Any, Dict 4 | 5 | from omni_bot_sdk.rpa.action_handlers import ( 6 | BaseActionHandler, 7 | RPAActionType, 8 | ) 9 | 10 | 11 | @dataclass 12 | class SwitchConversationAction: 13 | """ 14 | 切换会话操作。 15 | Attributes: 16 | target (str): 目标用户或群聊的标识。 17 | is_chatroom (bool): 是否为群聊。 18 | """ 19 | 20 | target: str = None 21 | is_chatroom: bool = False 22 | 23 | def __post_init__(self): 24 | self.action_type = RPAActionType.SWITCH_CONVERSATION 25 | self.is_send_message = False 26 | 27 | def to_dict(self) -> Dict[str, Any]: 28 | return { 29 | "action_type": self.action_type.value, 30 | "timestamp": self.timestamp.isoformat(), 31 | } 32 | 33 | 34 | class SwitchConversationHandler(BaseActionHandler): 35 | """ 36 | 切换会话操作的处理器。 37 | """ 38 | 39 | def execute(self, action: SwitchConversationAction) -> bool: 40 | """ 41 | 执行切换会话操作。 42 | Args: 43 | action (SwitchConversationAction): 操作对象。 44 | Returns: 45 | bool: 操作是否成功。 46 | """ 47 | try: 48 | return self.window_manager.switch_session(action.target) 49 | finally: 50 | self._cleanup() 51 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/rpa/message_sender.py: -------------------------------------------------------------------------------- 1 | """ 2 | 消息发送模块。 3 | 提供自动化消息发送、@用户、剪贴板图片处理等能力。 4 | """ 5 | 6 | import logging 7 | import time 8 | from typing import Dict, List, Optional, Tuple 9 | 10 | import pyautogui 11 | import win32clipboard 12 | import win32con 13 | from Levenshtein import ratio 14 | from omni_bot_sdk.rpa.image_processor import ImageProcessor 15 | from omni_bot_sdk.rpa.ocr_processor import OCRProcessor 16 | from omni_bot_sdk.rpa.window_manager import WindowManager 17 | from omni_bot_sdk.utils.helpers import ( 18 | copy_file_to_clipboard, 19 | get_center_point, 20 | read_temp_image, 21 | save_clipboard_image_to_temp, 22 | set_clipboard_text, 23 | ) 24 | from omni_bot_sdk.utils.mouse import human_like_mouse_move 25 | from PIL import Image 26 | 27 | 28 | class MessageSender: 29 | """ 30 | 消息发送器。 31 | 支持文本消息、@用户、剪贴板图片等自动化发送。 32 | """ 33 | 34 | def __init__(self, window_manager: WindowManager): 35 | """ 36 | 初始化 MessageSender。 37 | Args: 38 | window_manager (WindowManager): 窗口管理器。 39 | """ 40 | self.logger = logging.getLogger(__name__) 41 | self.ocr_processor = None 42 | self.temp_image_path = None 43 | self.window_manager = window_manager 44 | 45 | def send_message(self, message: str, clear_input_box: bool = True) -> bool: 46 | """ 47 | 发送文本消息。 48 | Args: 49 | message (str): 消息内容。 50 | clear_input_box (bool): 是否先清空输入框。 51 | Returns: 52 | bool: 是否发送成功。 53 | """ 54 | try: 55 | if not self.window_manager.activate_input_box(): 56 | return False 57 | if clear_input_box: 58 | pyautogui.hotkey("ctrl", "a") 59 | time.sleep(0.3) 60 | pyautogui.press("delete") 61 | time.sleep(0.3) 62 | if not set_clipboard_text(message): 63 | return False 64 | pyautogui.hotkey("ctrl", "v") 65 | time.sleep(0.3) 66 | send_button = self.window_manager.get_icon_position("send_button") 67 | if send_button: 68 | center = get_center_point(send_button) 69 | pyautogui.click(center[0], center[1]) 70 | time.sleep(0.3) 71 | return True 72 | return False 73 | except Exception as e: 74 | self.logger.error(f"发送消息时出错: {str(e)}") 75 | return False 76 | 77 | def _calc_similarity( 78 | self, search_text: str, formatted_results: List[Dict], score_cutoff: float = 0.6 79 | ) -> List[Dict]: 80 | """ 81 | 计算文本相似度。 82 | Args: 83 | search_text (str): 查询文本。 84 | formatted_results (List[Dict]): OCR 结果。 85 | score_cutoff (float): 相似度阈值。 86 | Returns: 87 | List[Dict]: 匹配结果。 88 | """ 89 | for result in formatted_results: 90 | result["similarity"] = ratio( 91 | search_text, 92 | result["label"], 93 | processor=lambda x: x.lower().replace(" ", ""), 94 | score_cutoff=score_cutoff, 95 | ) 96 | return [ 97 | result 98 | for result in formatted_results 99 | if result["similarity"] >= float(score_cutoff) 100 | ] 101 | 102 | def read_temp_image(self, image_path: str) -> bool: 103 | """ 104 | 读取临时图片。 105 | Args: 106 | image_path (str): 图片路径。 107 | Returns: 108 | bool: 是否成功。 109 | """ 110 | return read_temp_image(image_path) 111 | 112 | def save_clipboard_image_to_temp(self) -> Optional[str]: 113 | """ 114 | 保存剪贴板图片到临时文件。 115 | Returns: 116 | Optional[str]: 文件路径。 117 | """ 118 | return save_clipboard_image_to_temp() 119 | 120 | def mention_user(self, at_str: str) -> bool: 121 | """ 122 | @用户。 123 | Args: 124 | at_str (str): 用户名。 125 | Returns: 126 | bool: 是否成功。 127 | """ 128 | self.window_manager.activate_input_box() 129 | pyautogui.press("@") 130 | time.sleep(0.3) 131 | at_str = at_str.split(" ") 132 | at_str = max(at_str, key=len) 133 | at_str = at_str.strip() 134 | if not set_clipboard_text(f"{at_str}_"): 135 | self.logger.error(f"设置剪贴板文本时出错") 136 | return False 137 | pyautogui.hotkey("ctrl", "v") 138 | pyautogui.press("space") 139 | pyautogui.press("backspace") 140 | pyautogui.press("backspace") 141 | time.sleep(0.3) 142 | pyautogui.press("enter") 143 | time.sleep(0.3) 144 | return True 145 | 146 | def clear_input_box(self) -> bool: 147 | """ 148 | 清空输入框。 149 | Returns: 150 | bool: 是否成功。 151 | """ 152 | self.window_manager.activate_input_box() 153 | pyautogui.hotkey("ctrl", "a") 154 | time.sleep(0.3) 155 | pyautogui.press("backspace") 156 | time.sleep(0.3) 157 | return True 158 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/services/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 服务层包初始化文件。 3 | 包含数据库、消息、RPA、微信等核心服务模块。 4 | """ 5 | 6 | try: 7 | from omni_bot_sdk.services.pro.new_friend_check_service import NewFriendCheckService 8 | except ImportError: 9 | from omni_bot_sdk.services.functional.new_friend_check_service import ( 10 | NewFriendCheckService, 11 | ) 12 | 13 | __all__ = ["NewFriendCheckService"] 14 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/services/core/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 核心服务子包初始化文件。 3 | 包含数据库、消息、用户、RPA等核心服务实现。 4 | """ 5 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/services/core/async_plugin_runner.py: -------------------------------------------------------------------------------- 1 | """ 2 | 异步插件运行器模块。 3 | 提供插件异步执行相关服务。 4 | """ 5 | 6 | import asyncio 7 | import logging 8 | import threading 9 | from queue import Queue 10 | 11 | from omni_bot_sdk.plugins.interface import ProcessorService 12 | from omni_bot_sdk.plugins.plugin_manager import PluginManager 13 | 14 | 15 | class AsyncPluginRunner: 16 | """ 17 | 一个在独立线程中运行 asyncio 事件循环的执行器, 18 | 用于从同步代码中调用异步插件。 19 | """ 20 | 21 | def __init__( 22 | self, plugin_manager: PluginManager, processor_service: "ProcessorService" 23 | ): 24 | self.logger = logging.getLogger(__name__) 25 | self.plugin_manager = plugin_manager 26 | self.processor_service = processor_service 27 | self.loop: asyncio.AbstractEventLoop = None 28 | self.thread: threading.Thread = None 29 | self.is_running = False 30 | 31 | def _run_loop(self): 32 | self.logger.info("异步执行器线程启动...") 33 | self.loop = asyncio.new_event_loop() 34 | asyncio.set_event_loop(self.loop) 35 | self.loop.run_forever() 36 | self.logger.info("异步事件循环已停止。") 37 | 38 | def start(self): 39 | if self.is_running: 40 | return 41 | self.is_running = True 42 | self.thread = threading.Thread(target=self._run_loop, daemon=True) 43 | self.thread.start() 44 | while self.loop is None or not self.loop.is_running(): 45 | pass # 等待事件循环真正启动 46 | self.logger.info("异步执行器已启动。") 47 | 48 | def stop(self): 49 | if not self.is_running: 50 | return 51 | self.loop.call_soon_threadsafe(self.loop.stop) 52 | self.thread.join() 53 | self.is_running = False 54 | self.logger.info("异步执行器已停止。") 55 | 56 | def submit_task(self, message, context: dict): 57 | """从任何线程安全地提交一个异步任务到事件循环。""" 58 | if not self.is_running: 59 | self.logger.warning("异步执行器未运行,任务无法提交。") 60 | return 61 | asyncio.run_coroutine_threadsafe( 62 | self._process_and_handle_result(message, context), self.loop 63 | ) 64 | 65 | async def _process_and_handle_result(self, message, context: dict): 66 | """在事件循环线程中实际执行插件处理的协程。""" 67 | try: 68 | responses = await self.plugin_manager.process_message(message, context) 69 | if responses: 70 | for response in responses: 71 | self.logger.info( 72 | f"插件 {response.plugin_name} (async) 处理结束,移交RPA,指令数量:{len(response.actions)}" 73 | ) 74 | if response.actions: 75 | self.processor_service.add_rpa_actions(response.actions) 76 | except Exception as e: 77 | self.logger.error(f"在异步插件处理中发生错误: {e}", exc_info=True) 78 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/services/core/database_service.cp312-win_amd64.pyd: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:68ac931855cd165500016fd917ec05bb93e4b1556faa904eed9ce573c2c4ff29 3 | size 546304 4 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/services/core/message_factory_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | 消息工厂服务模块。 3 | 提供消息工厂相关的服务接口。 4 | """ 5 | 6 | import logging 7 | 8 | from omni_bot_sdk.models import UserInfo 9 | from omni_bot_sdk.services.core.database_service import DatabaseService 10 | from omni_bot_sdk.weixin.message_classes import Message, MessageType 11 | from omni_bot_sdk.weixin.message_factory import FACTORY_REGISTRY 12 | 13 | 14 | class MessageFactoryService: 15 | def __init__(self, user_info: UserInfo, db: DatabaseService): 16 | self.logger = logging.getLogger(__name__) 17 | self.user_info = user_info 18 | self.db = db 19 | 20 | def create_message(self, message: tuple) -> Message: 21 | """将消息转换为Message对象""" 22 | # TODO 加缓存,考虑到复杂程度,先不加了,腾讯在sqlite中索引加的不少,测试直接查询速度不慢 23 | table_name, msg_with_db = message 24 | type_ = msg_with_db[2] 25 | self.logger.info(f"消息类型: {MessageType.name(type_)}") 26 | room = self.db.get_room_by_md5(table_name.replace("Msg_", "")) 27 | if type_ not in FACTORY_REGISTRY: 28 | type_ = -1 29 | if type_ == -1: 30 | self.logger.error(f"该消息类型: {type_} 未找到对应的工厂") 31 | return None 32 | contact = self.db.get_contact_by_sender_id(msg_with_db[4], msg_with_db[17]) 33 | if not contact: 34 | self.logger.warn(f"未找到联系人: {msg_with_db[4]}") 35 | # TODO 有些消息是允许没有发送人的?这个时候怎么搞?是不是把他当作系统呢? 36 | msg = FACTORY_REGISTRY[type_].create( 37 | msg_with_db, self.user_info, self.db, contact, room 38 | ) 39 | msg.room = room 40 | if contact: 41 | msg.contact = contact 42 | return msg 43 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/services/core/message_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | 消息服务模块。 3 | 提供消息的存储、检索、分发等服务。 4 | """ 5 | 6 | import logging 7 | import threading 8 | import time 9 | from queue import Empty, Queue 10 | from typing import Callable, Optional 11 | from pathlib import Path 12 | from omni_bot_sdk.services.core.database_service import DatabaseService 13 | 14 | 15 | class MessageService: 16 | def __init__(self, message_queue: Queue, db: DatabaseService): 17 | self.logger = logging.getLogger(__name__) 18 | self.message_queue = message_queue 19 | self.db = db 20 | self.is_running = False 21 | self.is_paused = False # 新增:用于标记是否暂停 22 | self.thread: Optional[threading.Thread] = None 23 | self.callback: Optional[Callable] = None 24 | 25 | def start(self): 26 | """启动监听器""" 27 | if self.is_running: 28 | self.logger.warning("监听器已经在运行中") 29 | return False 30 | 31 | self.is_running = True 32 | self.thread = threading.Thread(target=self._message_loop) 33 | self.thread.daemon = True 34 | self.thread.start() 35 | self.logger.info("监听器已启动") 36 | return True 37 | 38 | def stop(self): 39 | """停止监听器""" 40 | if not self.is_running: 41 | self.logger.warning("监听器未在运行") 42 | return False 43 | 44 | self.is_running = False 45 | if self.thread: 46 | self.thread.join() 47 | self.logger.info("监听器已停止") 48 | return True 49 | 50 | def set_callback(self, callback: Callable): 51 | """设置消息回调函数""" 52 | self.callback = callback 53 | 54 | def pause(self): 55 | """ 56 | 暂停消息获取 57 | """ 58 | if not self.is_running or self.is_paused: 59 | self.logger.info("消息监听器已暂停或未运行,无需重复暂停。") 60 | return 61 | self.is_paused = True 62 | self.logger.info("消息监听器已暂停。") 63 | 64 | def resume(self): 65 | """ 66 | 恢复消息获取 67 | """ 68 | if not self.is_running or not self.is_paused: 69 | self.logger.info("消息监听器未暂停或未运行,无需恢复。") 70 | return 71 | self.is_paused = False 72 | self.logger.info("消息监听器已恢复。") 73 | 74 | def _message_loop(self): 75 | """监听循环""" 76 | while self.is_running: 77 | if self.is_paused: 78 | time.sleep(1) 79 | continue 80 | try: 81 | message = self.db.check_new_messages() 82 | if message: 83 | for msg in message: 84 | self.logger.info( 85 | f"新消息插入队列,来自于{Path(msg[1][-1]).name} : {msg[0]}" 86 | ) 87 | self.message_queue.put(msg) 88 | self.logger.info(f"消息队列大小: {self.message_queue.qsize()}") 89 | # 保存消息到数据库 90 | if self.callback: 91 | self.callback(message) 92 | time.sleep(0.75) 93 | except Empty: 94 | # 队列为空,继续下一次循环 95 | time.sleep(1) # 96 | continue 97 | except Exception as e: 98 | if self.is_running: # 忽略超时异常 99 | self.logger.error(f"处理消息时出错: {e}") 100 | time.sleep(1) # 101 | 102 | def get_status(self) -> dict: 103 | """获取监听器状态""" 104 | return {"is_running": self.is_running, "queue_size": self.message_queue.qsize()} 105 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/services/core/rpa_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | RPA 服务模块。 3 | 提供 RPA 相关的服务接口。 4 | """ 5 | 6 | import logging 7 | import threading 8 | import time 9 | from queue import Empty, Queue 10 | from typing import Any, Optional 11 | 12 | from omni_bot_sdk.rpa.action_handlers import RPAAction 13 | 14 | # 导入依赖的类型,用于类型提示 15 | from omni_bot_sdk.rpa.controller import RPAController 16 | 17 | 18 | class RPAService: 19 | """ 20 | RPA服务【任务消费者】。 21 | 它作为一个后台服务,在独立的线程中运行。 22 | 其唯一职责是从RPA任务队列中按顺序取出任务,并将其交给RPAController执行。 23 | """ 24 | 25 | def __init__(self, rpa_task_queue: Queue, rpa_controller: RPAController): 26 | """ 27 | 【轻量级初始化】只接收并保存已创建好的依赖项。 28 | 29 | Args: 30 | rpa_task_queue (Queue[RPAAction]): 用于接收RPA任务的队列。 31 | rpa_controller (RPAController): 负责实际执行RPA操作的控制器实例。 32 | """ 33 | self.logger = logging.getLogger(self.__class__.__name__) 34 | 35 | # --- 1. 保存注入的依赖 --- 36 | self.task_queue: Queue = rpa_task_queue 37 | self.controller: RPAController = rpa_controller 38 | 39 | # --- 2. 初始化自身状态 --- 40 | self.is_running: bool = False 41 | self.thread: Optional[threading.Thread] = None 42 | 43 | def start(self): 44 | """ 45 | 启动RPA服务的后台线程。 46 | 由Bot的生命周期管理器调用。 47 | """ 48 | if self.is_running: 49 | self.logger.warning("RPAService is already running.") 50 | return 51 | 52 | self.logger.info("Starting RPAService...") 53 | self.is_running = True 54 | self.thread = threading.Thread( 55 | target=self._execute_loop, name="RPAWorkerThread" 56 | ) 57 | self.thread.daemon = True 58 | self.thread.start() 59 | self.logger.info("RPAService has started.") 60 | 61 | def stop(self): 62 | """ 63 | 停止RPA服务的后台线程。 64 | 由Bot的生命周期管理器调用。 65 | """ 66 | if not self.is_running: 67 | self.logger.warning("RPAService is not running.") 68 | return 69 | 70 | self.logger.info("Stopping RPAService...") 71 | self.is_running = False 72 | if self.thread and self.thread.is_alive(): 73 | # 等待线程自然结束,可以设置一个超时 74 | self.thread.join(timeout=5.0) 75 | if self.thread.is_alive(): 76 | self.logger.warning("RPAWorkerThread did not terminate gracefully.") 77 | self.thread = None 78 | self.logger.info("RPAService has stopped.") 79 | 80 | def _execute_loop(self): 81 | """ 82 | 执行循环。这是运行在后台线程中的核心逻辑。 83 | """ 84 | self.logger.info("RPA worker loop started.") 85 | while self.is_running: 86 | try: 87 | # 从队列中阻塞式地获取任务,设置超时以响应停止信号 88 | task = self.task_queue.get(block=True, timeout=1.0) 89 | 90 | if task: 91 | self.logger.debug(f"Processing RPA action: {task.action_type.name}") 92 | # 将任务交给RPAController执行 93 | self.controller.execute_action(task) 94 | self.task_queue.task_done() 95 | 96 | except Empty: 97 | # 队列为空是正常情况,继续循环以检查 is_running 状态 98 | continue 99 | except Exception as e: 100 | self.logger.error( 101 | f"An unhandled exception occurred in RPA worker loop: {e}", 102 | exc_info=True, 103 | ) 104 | # 发生错误时等待一小段时间,避免CPU空转 105 | time.sleep(1) 106 | self.logger.info("RPA worker loop finished.") 107 | 108 | # 移除了 submit_task 方法,因为任务提交的职责现在完全由Bot类承担, 109 | # Bot会直接访问它持有的 rpa_task_queue 来提交任务。 110 | # 这确保了任务提交的入口是唯一的。 111 | 112 | # get_status 方法可以保留,用于监控 113 | def get_status(self) -> dict: 114 | """ 115 | 获取RPA服务的当前状态。 116 | """ 117 | status = { 118 | "is_running": self.is_running, 119 | "queue_size": self.task_queue.qsize(), 120 | } 121 | # 如果RPAController有get_status方法,也可以在这里调用 122 | if hasattr(self.controller, "get_status"): 123 | status.update(self.controller.get_status()) 124 | return status 125 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/services/core/user_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | 用户服务模块。 3 | 提供用户相关的服务接口。 4 | """ 5 | 6 | import json 7 | import os 8 | from pathlib import Path 9 | from typing import Optional 10 | 11 | from omni_bot_sdk.models import UserInfo 12 | from omni_bot_sdk.utils.fuck_zxl import WeChatDumper 13 | 14 | 15 | class UserService: 16 | """ 17 | 用户服务类。 18 | 管理用户信息和授权信息。 19 | """ 20 | 21 | def __init__(self, dbkey: str): 22 | """ 23 | 初始化用户服务。 24 | 25 | Args: 26 | dbkey: 数据库键。 27 | """ 28 | self.dbkey = dbkey 29 | self.user_info: UserInfo = None 30 | self.wxdump = WeChatDumper() 31 | wechat_info = self.wxdump.find_and_dump() 32 | if wechat_info: 33 | self.user_info = UserInfo( 34 | pid=wechat_info.pid, 35 | version=wechat_info.version, 36 | account=wechat_info.account, 37 | alias=wechat_info.alias, 38 | nickname=wechat_info.nickname, 39 | phone=wechat_info.phone, 40 | data_dir=wechat_info.data_dir, 41 | dbkey=self.dbkey, 42 | raw_keys={}, 43 | dat_key="", 44 | dat_xor_key=-1, 45 | avatar_url=wechat_info.avatar_url, 46 | ) 47 | else: 48 | raise Exception("未找到微信主窗口,请确保微信已登录") 49 | 50 | def get_user_info(self): 51 | """ 52 | 获取当前用户信息。 53 | 54 | Returns: 55 | 用户信息。 56 | """ 57 | return self.user_info 58 | 59 | def set_user_info(self, user_info: UserInfo): 60 | """ 61 | 更新用户信息。 62 | 63 | Args: 64 | user_info: 新的用户信息。 65 | """ 66 | self.user_info = user_info 67 | 68 | def update_raw_key(self, key: str, value: str): 69 | """ 70 | 更新原始密钥。 71 | 72 | Args: 73 | key: 密钥名称。 74 | value: 密钥值。 75 | """ 76 | self.user_info.raw_keys[key] = value 77 | 78 | def get_raw_key(self, key: str) -> Optional[str]: 79 | """ 80 | 获取原始密钥。 81 | 82 | Args: 83 | key: 密钥名称。 84 | 85 | Returns: 86 | 密钥值,如果不存在则返回None。 87 | """ 88 | return self.user_info.raw_keys.get(key, None) 89 | 90 | def dump_to_file(self): 91 | """ 92 | 将当前用户信息写入到Windows用户目录下,文件名为account.json,使用pathlib实现。 93 | """ 94 | if not self.user_info: 95 | raise Exception("用户信息未初始化") 96 | # 获取用户目录 97 | user_home = Path.home() 98 | # 构造文件路径 99 | file_path = user_home / f"{self.user_info.account}.json" 100 | # 转为dict并写入json 101 | with open(file_path, "w", encoding="utf-8") as f: 102 | json.dump(self.user_info.to_dict(), f, ensure_ascii=False, indent=4) 103 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/services/functional/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 功能服务子包初始化文件。 3 | 包含微信状态、数据解密、新好友校验等功能服务。 4 | """ 5 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/services/functional/new_friend_check_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | 新好友校验服务模块。 3 | 提供新好友添加、校验等相关服务。 4 | """ 5 | 6 | from queue import Queue 7 | from omni_bot_sdk.services.core.database_service import DatabaseService 8 | 9 | 10 | class NewFriendCheckService: 11 | """ 12 | 开源版本 占位 13 | """ 14 | 15 | def __init__(self, rpa_queue: Queue, db: DatabaseService): 16 | pass 17 | 18 | def start(self): 19 | pass 20 | 21 | def stop(self): 22 | pass 23 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 通用工具包初始化文件。 3 | 包含自动化相关的常用工具模块。 4 | """ 5 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/utils/fuck_zxl.cp312-win_amd64.pyd: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:2d98f63703672831c746ead9997b1567b62c63e13dc84b951b33434cc983b3b4 3 | size 546304 4 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/utils/logging_setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | 日志配置模块。 3 | 提供日志初始化与格式化工具。 4 | """ 5 | 6 | import logging 7 | import os 8 | from logging.handlers import RotatingFileHandler 9 | from pathlib import Path 10 | 11 | import colorlog 12 | 13 | 14 | def setup_logging(log_dir: str = "logs", log_level: int = logging.INFO): 15 | """ 16 | 配置日志系统 17 | 18 | Args: 19 | log_dir: 日志目录 20 | log_level: 日志级别 21 | """ 22 | root_logger = logging.getLogger() 23 | if root_logger.handlers: 24 | return 25 | # 创建日志目录 26 | log_path = Path(log_dir) 27 | log_path.mkdir(parents=True, exist_ok=True) 28 | 29 | # 配置根日志记录器 30 | root_logger.setLevel(log_level) 31 | 32 | # 创建彩色控制台处理器 33 | console_handler = colorlog.StreamHandler() 34 | console_handler.setLevel(log_level) 35 | 36 | # 彩色日志格式,增加毫秒 37 | console_formatter = colorlog.ColoredFormatter( 38 | "%(log_color)s%(asctime)s.%(msecs)03d - %(levelname)s - %(module)s:%(lineno)d - %(message)s", 39 | datefmt="%H:%M:%S", 40 | log_colors={ 41 | "DEBUG": "cyan", 42 | "INFO": "green", 43 | "WARNING": "yellow", 44 | "ERROR": "red", 45 | "CRITICAL": "red,bg_white", 46 | }, 47 | ) 48 | console_handler.setFormatter(console_formatter) 49 | root_logger.addHandler(console_handler) 50 | 51 | # 创建文件处理器 52 | file_handler = RotatingFileHandler( 53 | log_path / "app.log", maxBytes=10 * 1024 * 1024, backupCount=5 # 10MB 54 | ) 55 | file_handler.setLevel(log_level) 56 | # 文件日志格式(使用模块名和行号) 57 | file_formatter = logging.Formatter( 58 | "%(asctime)s - %(name)s - %(levelname)s - %(module)s:%(lineno)d - %(message)s", 59 | datefmt="%Y-%m-%d %H:%M:%S", 60 | ) 61 | file_handler.setFormatter(file_formatter) 62 | root_logger.addHandler(file_handler) 63 | 64 | # 设置第三方库的日志级别 65 | logging.getLogger("urllib3").setLevel(logging.WARNING) 66 | logging.getLogger("werkzeug").setLevel(logging.WARNING) 67 | logging.getLogger("ultralytics").setLevel(logging.ERROR) 68 | logging.getLogger("RapidOCR").setLevel(logging.WARNING) 69 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/utils/mouse.py: -------------------------------------------------------------------------------- 1 | """ 2 | 鼠标操作工具模块。 3 | 提供人类化鼠标移动、点击等自动化能力。 4 | """ 5 | 6 | import math 7 | import random 8 | import time 9 | 10 | import pyautogui 11 | 12 | # --- Default Configuration Parameters --- 13 | # You can override these by passing arguments to the main function 14 | 15 | # Range for base speed (pixels per second) 16 | DEFAULT_SPEED_RANGE = ( 17 | 700, 18 | 1200, 19 | ) # e.g., mouse moves between 700 and 1200 pixels/sec on average 20 | 21 | # List of available tweening functions 22 | DEFAULT_TWEEN_FUNCTIONS = [ 23 | pyautogui.easeInQuad, 24 | pyautogui.easeOutQuad, 25 | pyautogui.easeInOutQuad, 26 | pyautogui.easeInBounce, 27 | pyautogui.easeInElastic, 28 | pyautogui.easeOutElastic, 29 | pyautogui.easeInOutElastic, 30 | pyautogui.easeOutBounce, 31 | # pyautogui.linear # Usually too robotic, but can be included for variety 32 | ] 33 | 34 | # Minimum movement duration (seconds) 35 | DEFAULT_MIN_DURATION = 0.1 36 | 37 | # Maximum movement duration (seconds) 38 | DEFAULT_MAX_DURATION = ( 39 | 1.0 # Adjusted from 1.2 to make it a bit quicker on average for long moves 40 | ) 41 | 42 | # Duration randomization factor range (multiplies the base calculated duration) 43 | DEFAULT_DURATION_RANDOM_FACTOR_RANGE = ( 44 | 0.75, 45 | 1.25, 46 | ) # e.g., 75% to 125% of calculated time 47 | 48 | 49 | def human_like_mouse_move( 50 | target_x, 51 | target_y, 52 | speed_range=DEFAULT_SPEED_RANGE, 53 | tween_functions=DEFAULT_TWEEN_FUNCTIONS, 54 | min_duration=DEFAULT_MIN_DURATION, 55 | max_duration=DEFAULT_MAX_DURATION, 56 | duration_random_factor_range=DEFAULT_DURATION_RANDOM_FACTOR_RANGE, 57 | verbose=False, # Set to True to print debug information 58 | ): 59 | """ 60 | 模拟人类鼠标移动行为。 61 | 62 | Args: 63 | target_x (int): 目标x坐标。 64 | target_y (int): 目标y坐标。 65 | speed_range (tuple): 鼠标速度范围 (最小像素/秒, 最大像素/秒)。 66 | tween_functions (list): 可选择的PyAutoGUI缓动函数列表。 67 | min_duration (float): 鼠标移动的最小持续时间。 68 | max_duration (float): 鼠标移动的最大持续时间。 69 | duration_random_factor_range (tuple): 持续时间随机化因子范围 (最小因子, 最大因子)。 70 | verbose (bool): 如果为True,则打印移动详情。 71 | """ 72 | current_x, current_y = pyautogui.position() 73 | 74 | # 1. Randomly select a base speed 75 | selected_base_speed = random.uniform(speed_range[0], speed_range[1]) 76 | 77 | # 2. Randomly select a tween function 78 | selected_tween = random.choice(tween_functions) 79 | 80 | # 3. Calculate distance 81 | distance = math.sqrt((target_x - current_x) ** 2 + (target_y - current_y) ** 2) 82 | 83 | # 4. Calculate base duration 84 | if distance == 0: 85 | # If already at the target, simulate a tiny "adjustment" or "hesitation" 86 | final_duration = random.uniform(min_duration * 0.5, min_duration * 1.5) 87 | # Optionally, don't move at all or move by a pixel and back 88 | # For now, we'll just use a small duration, moveTo will handle no actual move 89 | else: 90 | base_duration = distance / selected_base_speed 91 | 92 | # 5. Apply randomization to duration 93 | random_factor = random.uniform( 94 | duration_random_factor_range[0], duration_random_factor_range[1] 95 | ) 96 | randomized_duration = base_duration * random_factor 97 | 98 | # 6. Ensure duration is within min/max bounds 99 | final_duration = max(min_duration, min(max_duration, randomized_duration)) 100 | 101 | if verbose: 102 | print( 103 | f"Human-like move from ({current_x},{current_y}) to ({target_x},{target_y}):" 104 | ) 105 | print(f" Distance: {distance:.2f} pixels") 106 | print(f" Selected Speed: {selected_base_speed:.2f} px/s") 107 | print(f" Selected Tween: {selected_tween.__name__}") 108 | print( 109 | f" Calculated Base Duration: {base_duration if distance > 0 else 'N/A':.3f}s" 110 | ) 111 | print( 112 | f" Randomized Duration (before clamp): {randomized_duration if distance > 0 else 'N/A':.3f}s" 113 | ) 114 | print(f" Final Duration: {final_duration:.3f}s") 115 | 116 | # 7. Execute the mouse move 117 | pyautogui.moveTo(target_x, target_y, duration=final_duration, tween=selected_tween) 118 | 119 | if verbose: 120 | final_pos = pyautogui.position() 121 | print(f" Moved to: ({final_pos[0]},{final_pos[1]})") 122 | print("-" * 30) 123 | 124 | 125 | # --- Example Usage --- 126 | if __name__ == "__main__": 127 | print("PyAutoGUI will start moving the mouse in 3 seconds...") 128 | print("Switch to a window where you can observe the mouse.") 129 | time.sleep(3) 130 | 131 | screen_width, screen_height = pyautogui.size() 132 | 133 | # Test Case 1: Short move 134 | print("\n--- Test Case 1: Short Move ---") 135 | human_like_mouse_move(100, 150, verbose=True) 136 | time.sleep(random.uniform(0.5, 1.5)) # Human-like pause 137 | 138 | # Test Case 2: Medium move to center 139 | print("\n--- Test Case 2: Medium Move to Center ---") 140 | human_like_mouse_move(screen_width // 2, screen_height // 2, verbose=True) 141 | time.sleep(random.uniform(0.5, 1.5)) 142 | 143 | # Test Case 3: Long move to bottom-right 144 | print("\n--- Test Case 3: Long Move to Bottom-Right ---") 145 | human_like_mouse_move(screen_width - 50, screen_height - 50, verbose=True) 146 | time.sleep(random.uniform(0.5, 1.5)) 147 | 148 | # Test Case 4: Move to current location (should be very quick) 149 | print("\n--- Test Case 4: Move to Current Location ---") 150 | current_x, current_y = pyautogui.position() 151 | human_like_mouse_move(current_x, current_y, verbose=True) 152 | time.sleep(random.uniform(0.5, 1.5)) 153 | 154 | # Test Case 5: Custom parameters - Slower speed, specific tweens 155 | print("\n--- Test Case 5: Custom Parameters (Slower) ---") 156 | custom_tweens = [pyautogui.easeInBounce, pyautogui.easeOutBounce] 157 | human_like_mouse_move( 158 | 200, 159 | screen_height - 200, 160 | speed_range=(300, 500), # Slower 161 | tween_functions=custom_tweens, 162 | min_duration=0.3, 163 | max_duration=2.0, 164 | verbose=True, 165 | ) 166 | 167 | print("\nAll tests completed.") 168 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/utils/size_config.py: -------------------------------------------------------------------------------- 1 | """ 2 | 窗口尺寸配置模块。 3 | 用于建议和管理窗口尺寸参数。 4 | """ 5 | 6 | import math 7 | from dataclasses import dataclass 8 | 9 | import pyautogui 10 | 11 | IMAGE_FACTOR = 28 12 | MIN_PIXELS = 4 * 28 * 28 13 | MAX_PIXELS = 16384 * 28 * 28 14 | MAX_RATIO = 200 15 | 16 | 17 | @dataclass 18 | class SizeConfig: 19 | width: int 20 | height: int 21 | msg_top_x: int 22 | msg_top_y: int 23 | msg_width: int 24 | msg_height: int 25 | menu_width: int 26 | menu_height: int 27 | 28 | 29 | def round_by_factor(number: int, factor: int) -> int: 30 | """ 31 | 将数字四舍五入到指定因子的倍数。 32 | 33 | Args: 34 | number: 要四舍五入的数字。 35 | factor: 因子。 36 | 37 | Returns: 38 | 四舍五入后的数字。 39 | """ 40 | return round(number / factor) * factor 41 | 42 | 43 | def ceil_by_factor(number: int, factor: int) -> int: 44 | """ 45 | 将数字向上取整到指定因子的倍数。 46 | 47 | Args: 48 | number: 要向上取整的数字。 49 | factor: 因子。 50 | 51 | Returns: 52 | 向上取整后的数字。 53 | """ 54 | return math.ceil(number / factor) * factor 55 | 56 | 57 | def floor_by_factor(number: int, factor: int) -> int: 58 | """ 59 | 将数字向下取整到指定因子的倍数。 60 | 61 | Args: 62 | number: 要向下取整的数字。 63 | factor: 因子。 64 | 65 | Returns: 66 | 向下取整后的数字。 67 | """ 68 | return math.floor(number / factor) * factor 69 | 70 | 71 | def smart_resize( 72 | height: int, 73 | width: int, 74 | factor: int = IMAGE_FACTOR, 75 | min_pixels: int = MIN_PIXELS, 76 | max_pixels: int = MAX_PIXELS, 77 | ) -> tuple[int, int]: 78 | """ 79 | 智能调整图像尺寸,使其满足以下条件: 80 | 81 | 1. 高度和宽度都是指定因子的倍数。 82 | 83 | 2. 总像素数在 ['min_pixels', 'max_pixels'] 范围内。 84 | 85 | 3. 图像的宽高比尽可能接近原始比例。 86 | 87 | Args: 88 | height: 原始图像高度。 89 | width: 原始图像宽度。 90 | factor: 因子,默认为 IMAGE_FACTOR。 91 | min_pixels: 最小像素数,默认为 MIN_PIXELS。 92 | max_pixels: 最大像素数,默认为 MAX_PIXELS。 93 | 94 | Returns: 95 | 调整后的高度和宽度。 96 | 97 | Raises: 98 | ValueError: 如果调整后的宽高比超过 MAX_RATIO。 99 | """ 100 | if max(height, width) / min(height, width) > MAX_RATIO: 101 | raise ValueError( 102 | f"绝对宽高比必须小于 {MAX_RATIO},当前为 {max(height, width) / min(height, width)}" 103 | ) 104 | h_bar = max(factor, round_by_factor(height, factor)) 105 | w_bar = max(factor, round_by_factor(width, factor)) 106 | if h_bar * w_bar > max_pixels: 107 | beta = math.sqrt((height * width) / max_pixels) 108 | h_bar = floor_by_factor(height / beta, factor) 109 | w_bar = floor_by_factor(width / beta, factor) 110 | elif h_bar * w_bar < min_pixels: 111 | beta = math.sqrt(min_pixels / (height * width)) 112 | h_bar = ceil_by_factor(height * beta, factor) 113 | w_bar = ceil_by_factor(width * beta, factor) 114 | return h_bar, w_bar 115 | 116 | 117 | def convert_qwen_size( 118 | bbox: tuple[int, int, int, int], height: int, width: int 119 | ) -> tuple[int, int, int, int]: 120 | """ 121 | 将QwenVL模型的边界框坐标转换为屏幕坐标。 122 | 123 | Args: 124 | bbox: QwenVL模型的边界框坐标 (x1, y1, x2, y2)。 125 | height: 原始图像高度。 126 | width: 原始图像宽度。 127 | 128 | Returns: 129 | 转换后的屏幕坐标边界框 (x1, y1, x2, y2)。 130 | """ 131 | input_height, input_width = smart_resize(height, width) 132 | abs_x1 = int(bbox[0] / input_width * width) 133 | abs_y1 = int(bbox[1] / input_height * height) 134 | abs_x2 = int(bbox[2] / input_width * width) 135 | abs_y2 = int(bbox[3] / input_height * height) 136 | return tuple([abs_x1, abs_y1, abs_x2, abs_y2]) 137 | 138 | 139 | # TODO 历史残留,暂时不改了,后续统一处理,核心是基于QwenVL系列模型的图片需要进行resize之后才能提高detect精度 140 | def suggest_size() -> SizeConfig: 141 | """ 142 | 根据当前屏幕大小,给出建议的窗口大小 143 | VL模型有最适合的尺寸,不能设置太大或者太小建议是28的倍数 144 | 同时要考虑底部状态栏的影响 145 | 宽度设置为屏幕宽度的一半,最大不能超过1008 146 | 高度设置为屏幕高度减去任务栏的高度,任务栏就认为是100吧 147 | 宽高都需要是28的倍数 148 | """ 149 | screen_size = pyautogui.size() 150 | width = screen_size.width // 2 151 | height = screen_size.height - 80 152 | width = 1008 153 | if height < 812: 154 | height = 812 155 | if height > 2000: 156 | height = 2000 157 | # input_height, input_width = smart_resize(height, width) 158 | return SizeConfig(width, height, 0, 0, 0, 0, 130, 450) 159 | 160 | 161 | if __name__ == "__main__": 162 | print(suggest_size()) 163 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 微信相关功能包初始化文件。 3 | 包含消息解析、数据结构等核心组件。 4 | """ 5 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/parser/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | @Time : 2024/12/11 1:26 6 | @Author : SiYuan 7 | @Email : 863909694@qq.com 8 | @File : MemoTrace-__init__.py.py 9 | @Description : 10 | """ 11 | 12 | if __name__ == "__main__": 13 | pass 14 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/parser/audio_parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | @Time : 2024/12/12 16:55 6 | @Author : SiYuan 7 | @Email : 863909694@qq.com 8 | @File : MemoTrace-audio_parser.py 9 | @Description : 10 | """ 11 | import xmltodict 12 | 13 | 14 | def parser_audio(xml_content): 15 | result = {"audio_length": 0, "audio_text": ""} 16 | xml_content = xml_content.strip() 17 | try: 18 | xml_dict = xmltodict.parse(xml_content) 19 | voice_length = ( 20 | xml_dict.get("msg", {}).get("voicemsg", {}).get("@voicelength", 0) 21 | ) 22 | audio_text = xml_dict.get("msg", {}).get("voicetrans", {}).get("@transtext", "") 23 | result = {"audio_length": voice_length, "audio_text": audio_text} 24 | except: 25 | if xml_content and ":" in xml_content: 26 | voice_length = int(xml_content.split(":")[1]) 27 | result = {"audio_length": voice_length} 28 | finally: 29 | return result 30 | 31 | 32 | if __name__ == "__main__": 33 | pass 34 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/parser/emoji_parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | @Time : 2024/12/12 18:10 6 | @Author : SiYuan 7 | @Email : 863909694@qq.com 8 | @File : MemoTrace-emoji_parser.py 9 | @Description : 10 | """ 11 | import base64 12 | import html 13 | import re 14 | import traceback 15 | 16 | import xmltodict 17 | from google.protobuf.json_format import MessageToDict 18 | 19 | from .util.protocbuf import emoji_desc_pb2 20 | 21 | 22 | def parser_emoji(xml_content): 23 | result = {"md5": 0, "url": "", "width": 0, "height": 0, "desc": ""} 24 | 25 | def extract_msg(text): 26 | # 使用正则表达式匹配第一个 标签及其内容 27 | pattern = r"(.*?)" 28 | match = re.search(pattern, text) 29 | return f"{match.group(0)}" if match else "" 30 | 31 | # xml_content = xml_content.strip().replace('&', '&') 32 | try: 33 | xml_dict = xmltodict.parse(xml_content) 34 | except: 35 | try: 36 | xml_content = extract_msg(xml_content) 37 | xml_dict = xmltodict.parse(xml_content) 38 | except: 39 | pass 40 | try: 41 | emoji_dic = xml_dict.get("msg", {}).get("emoji", {}) 42 | if "@androidmd5" in emoji_dic: 43 | md5 = emoji_dic.get("@androidmd5", "") 44 | else: 45 | md5 = emoji_dic.get("@md5", "") 46 | # logger.error(xml_dict) 47 | desc_bs64 = emoji_dic.get("@desc", "") 48 | desc = "" 49 | if desc_bs64: 50 | # 逆天微信,竟然把protobuf数据用base64编码后放入xml里 51 | desc_bytes_proto = base64.b64decode(desc_bs64) 52 | message = emoji_desc_pb2.EmojiDescData() 53 | # 解析二进制数据 54 | message.ParseFromString(desc_bytes_proto) 55 | dict_output = MessageToDict(message) 56 | for item in dict_output.get("descItem", []): 57 | desc = item.get("desc", "") 58 | if desc: 59 | break 60 | # url 需要 urldecode 61 | url = emoji_dic.get("@cdnurl", "") 62 | if url: 63 | url = html.unescape(url) 64 | result = { 65 | "md5": md5, 66 | "url": url, 67 | "width": emoji_dic.get("@width", 0), 68 | "height": emoji_dic.get("@height", 0), 69 | "desc": desc, 70 | } 71 | except: 72 | print(traceback.format_exc()) 73 | print(xml_content) 74 | finally: 75 | return result 76 | 77 | 78 | if __name__ == "__main__": 79 | pass 80 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/parser/file_parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | @Time : 2024/12/12 22:52 6 | @Author : SiYuan 7 | @Email : 863909694@qq.com 8 | @File : MemoTrace-file_parser.py 9 | @Description : 10 | """ 11 | 12 | import xmltodict 13 | 14 | 15 | def get_image_type(header): 16 | # 根据文件头判断图片类型 17 | if header.startswith(b"\xff\xd8"): 18 | return "jpeg" 19 | elif header.startswith(b"\x89PNG"): 20 | return "png" 21 | elif header[:6] in (b"GIF87a", b"GIF89a"): 22 | return "gif" 23 | elif header.startswith(b"BM"): 24 | return "bmp" 25 | elif header.startswith(b"\x00\x00\x01\x00"): 26 | return "ico" 27 | elif header.startswith(b"\x49\x49\x2a\x00") or header.startswith( 28 | b"\x4d\x4d\x00\x2a" 29 | ): 30 | return "tiff" 31 | elif header.startswith(b"RIFF") and header[8:12] == b"WEBP": 32 | return "webp" 33 | else: 34 | return "png" 35 | 36 | 37 | def parse_video(xml_content): 38 | result = {"md5": 0} 39 | xml_content = xml_content.strip() 40 | try: 41 | xml_dict = xmltodict.parse(xml_content) 42 | # logger.error(json.dumps(xml_dict)) 43 | video_dic = xml_dict.get("msg", {}).get("videomsg", {}) 44 | md5 = video_dic.get("@md5", "") # 下载后压缩视频的md5 45 | rawmd5 = video_dic.get("@rawmd5", "") # 原视频md5 46 | result = { 47 | "md5": md5, 48 | "rawmd5": rawmd5, 49 | "length": video_dic.get("@playlength", 0), 50 | "size": video_dic.get("@length", 0), 51 | } 52 | except: 53 | print(f"视频解析失败\n{xml_content}") 54 | finally: 55 | return result 56 | 57 | 58 | if __name__ == "__main__": 59 | pass 60 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/parser/util/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/parser/util/protocbuf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weixin-omni/omni-bot-sdk-oss/7c73235fe5d551a35c396b87a419615847239eec/src/omni_bot_sdk/weixin/parser/util/protocbuf/__init__.py -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/parser/util/protocbuf/contact.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package example; 4 | 5 | // 顶级消息定义 6 | message ContactInfo { 7 | // varint 类型字段,根据数值范围选用 uint32 或 uint64 8 | uint32 gender = 2; // 性别:1 男 2:女 0:未知 9 | uint32 field3 = 3; 10 | string signature = 4; // 自助者天助!!! 11 | string country = 5; // CN 12 | string province = 6; // Shaanxi 13 | string city = 7; // Xi'an 14 | uint32 field8 = 8; 15 | string field9 = 9; 16 | uint32 field10 = 10; // 4294967295 17 | uint32 field11 = 11; 18 | uint32 field12 = 12; 19 | 20 | // 修改后的嵌套消息,对应 JSON 中 field 14 的数据结构 21 | MessageField14 phone_info = 14; 22 | 23 | string field15 = 15; 24 | uint32 field16 = 16; 25 | uint32 field17 = 17; 26 | uint32 field18 = 18; 27 | uint32 field19 = 19; 28 | string field20 = 20; 29 | string field21 = 21; 30 | uint32 field22 = 22; 31 | uint32 field23 = 23; 32 | uint32 field24 = 24; 33 | string field25 = 25; 34 | string field26 = 26; 35 | 36 | // 嵌套消息,朋友圈背景 37 | MessageField27 moments_info = 27; 38 | 39 | string field28 = 28; 40 | string field29 = 29; 41 | string label_list = 30; 42 | string field31 = 31; 43 | string field32 = 32; 44 | 45 | // 嵌套消息,对应 JSON 中 field 33 的 length_delimited 数据 46 | MessageField33 field33 = 33; 47 | 48 | string field34 = 34; 49 | string field35 = 35; 50 | MessageField36 field36 = 36; 51 | uint32 field37 = 37; 52 | uint32 field38 = 38; // 4294967295 53 | } 54 | 55 | // 定义 field14 对应的嵌套消息 56 | // 修改后的嵌套消息,用于 field 14 57 | message MessageField14 { 58 | uint32 field1 = 1; // varint 类型字段,存储数字 59 | repeated MessageField14_Result2 field2 = 2; // 这是一个 length_delimited 类型的字段,包含多个结果 60 | } 61 | 62 | 63 | message MessageField14_Result2 { 64 | string phone_numer = 1; // string 类型字段,存储电话号码 65 | } 66 | 67 | // 定义 field27 对应的嵌套消息 68 | 69 | message MessageField27 { 70 | uint32 field1 = 1; 71 | string background_url = 2; // 图片 URL 72 | uint64 field3 = 3; // 14588734692813845087(大数,用 uint64) 73 | uint32 field4 = 4; // 6785 74 | uint32 field5 = 5; // 4320 75 | } 76 | 77 | // 定义 field33 对应的嵌套消息 78 | 79 | message MessageField33 { 80 | string field1 = 1; 81 | } 82 | 83 | message MessageField36 { 84 | MessageField36_Result results = 1; 85 | } 86 | 87 | message MessageField36_Result { 88 | string field1 = 1; 89 | } -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/parser/util/protocbuf/contact_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: contact.proto 4 | """Generated protocol buffer code.""" 5 | from google.protobuf import descriptor as _descriptor 6 | from google.protobuf import descriptor_pool as _descriptor_pool 7 | from google.protobuf import symbol_database as _symbol_database 8 | from google.protobuf.internal import builder as _builder 9 | 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( 16 | b'\n\rcontact.proto\x12\x07\x65xample"\xd7\x05\n\x0b\x43ontactInfo\x12\x0e\n\x06gender\x18\x02 \x01(\r\x12\x0e\n\x06\x66ield3\x18\x03 \x01(\r\x12\x11\n\tsignature\x18\x04 \x01(\t\x12\x0f\n\x07\x63ountry\x18\x05 \x01(\t\x12\x10\n\x08province\x18\x06 \x01(\t\x12\x0c\n\x04\x63ity\x18\x07 \x01(\t\x12\x0e\n\x06\x66ield8\x18\x08 \x01(\r\x12\x0e\n\x06\x66ield9\x18\t \x01(\t\x12\x0f\n\x07\x66ield10\x18\n \x01(\r\x12\x0f\n\x07\x66ield11\x18\x0b \x01(\r\x12\x0f\n\x07\x66ield12\x18\x0c \x01(\r\x12+\n\nphone_info\x18\x0e \x01(\x0b\x32\x17.example.MessageField14\x12\x0f\n\x07\x66ield15\x18\x0f \x01(\t\x12\x0f\n\x07\x66ield16\x18\x10 \x01(\r\x12\x0f\n\x07\x66ield17\x18\x11 \x01(\r\x12\x0f\n\x07\x66ield18\x18\x12 \x01(\r\x12\x0f\n\x07\x66ield19\x18\x13 \x01(\r\x12\x0f\n\x07\x66ield20\x18\x14 \x01(\t\x12\x0f\n\x07\x66ield21\x18\x15 \x01(\t\x12\x0f\n\x07\x66ield22\x18\x16 \x01(\r\x12\x0f\n\x07\x66ield23\x18\x17 \x01(\r\x12\x0f\n\x07\x66ield24\x18\x18 \x01(\r\x12\x0f\n\x07\x66ield25\x18\x19 \x01(\t\x12\x0f\n\x07\x66ield26\x18\x1a \x01(\t\x12(\n\x07\x66ield27\x18\x1b \x01(\x0b\x32\x17.example.MessageField27\x12\x0f\n\x07\x66ield28\x18\x1c \x01(\t\x12\x0f\n\x07\x66ield29\x18\x1d \x01(\t\x12\x12\n\nlabel_list\x18\x1e \x01(\t\x12\x0f\n\x07\x66ield31\x18\x1f \x01(\t\x12\x0f\n\x07\x66ield32\x18 \x01(\t\x12(\n\x07\x66ield33\x18! \x01(\x0b\x32\x17.example.MessageField33\x12\x0f\n\x07\x66ield34\x18" \x01(\t\x12\x0f\n\x07\x66ield35\x18# \x01(\t\x12(\n\x07\x66ield36\x18$ \x01(\x0b\x32\x17.example.MessageField36\x12\x0f\n\x07\x66ield37\x18% \x01(\r\x12\x0f\n\x07\x66ield38\x18& \x01(\r"Q\n\x0eMessageField14\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\r\x12/\n\x06\x66ield2\x18\x02 \x03(\x0b\x32\x1f.example.MessageField14_Result2"-\n\x16MessageField14_Result2\x12\x13\n\x0bphone_numer\x18\x01 \x01(\t"h\n\x0eMessageField27\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\r\x12\x16\n\x0e\x62\x61\x63kground_url\x18\x02 \x01(\t\x12\x0e\n\x06\x66ield3\x18\x03 \x01(\x04\x12\x0e\n\x06\x66ield4\x18\x04 \x01(\r\x12\x0e\n\x06\x66ield5\x18\x05 \x01(\r" \n\x0eMessageField33\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\t"A\n\x0eMessageField36\x12/\n\x07results\x18\x01 \x01(\x0b\x32\x1e.example.MessageField36_Result"\'\n\x15MessageField36_Result\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\tb\x06proto3' 17 | ) 18 | 19 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) 20 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "contact_pb2", globals()) 21 | if _descriptor._USE_C_DESCRIPTORS == False: 22 | 23 | DESCRIPTOR._options = None 24 | _CONTACTINFO._serialized_start = 27 25 | _CONTACTINFO._serialized_end = 754 26 | _MESSAGEFIELD14._serialized_start = 756 27 | _MESSAGEFIELD14._serialized_end = 837 28 | _MESSAGEFIELD14_RESULT2._serialized_start = 839 29 | _MESSAGEFIELD14_RESULT2._serialized_end = 884 30 | _MESSAGEFIELD27._serialized_start = 886 31 | _MESSAGEFIELD27._serialized_end = 990 32 | _MESSAGEFIELD33._serialized_start = 992 33 | _MESSAGEFIELD33._serialized_end = 1024 34 | _MESSAGEFIELD36._serialized_start = 1026 35 | _MESSAGEFIELD36._serialized_end = 1091 36 | _MESSAGEFIELD36_RESULT._serialized_start = 1093 37 | _MESSAGEFIELD36_RESULT._serialized_end = 1132 38 | # @@protoc_insertion_point(module_scope) 39 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/parser/util/protocbuf/emoji_desc.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package example; 4 | 5 | message EmojiDescData { 6 | repeated EmojiDescItem descItem = 1; 7 | } 8 | 9 | message EmojiDescItem { 10 | string language = 1; 11 | string desc = 2; 12 | } -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/parser/util/protocbuf/emoji_desc_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: emoji_desc.proto 4 | """Generated protocol buffer code.""" 5 | from google.protobuf import descriptor as _descriptor 6 | from google.protobuf import descriptor_pool as _descriptor_pool 7 | from google.protobuf import symbol_database as _symbol_database 8 | from google.protobuf.internal import builder as _builder 9 | 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( 16 | b'\n\x10\x65moji_desc.proto\x12\x07\x65xample"9\n\rEmojiDescData\x12(\n\x08\x64\x65scItem\x18\x01 \x03(\x0b\x32\x16.example.EmojiDescItem"/\n\rEmojiDescItem\x12\x10\n\x08language\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x65sc\x18\x02 \x01(\tb\x06proto3' 17 | ) 18 | 19 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) 20 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "emoji_desc_pb2", globals()) 21 | if _descriptor._USE_C_DESCRIPTORS == False: 22 | 23 | DESCRIPTOR._options = None 24 | _EMOJIDESCDATA._serialized_start = 29 25 | _EMOJIDESCDATA._serialized_end = 86 26 | _EMOJIDESCITEM._serialized_start = 88 27 | _EMOJIDESCITEM._serialized_end = 135 28 | # @@protoc_insertion_point(module_scope) 29 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/parser/util/protocbuf/file_info.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package example; 4 | 5 | message FileInfoData { 6 | string dir3 = 1; 7 | uint32 file_size = 2; 8 | } 9 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/parser/util/protocbuf/file_info_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: file_info.proto 4 | """Generated protocol buffer code.""" 5 | from google.protobuf import descriptor as _descriptor 6 | from google.protobuf import descriptor_pool as _descriptor_pool 7 | from google.protobuf import symbol_database as _symbol_database 8 | from google.protobuf.internal import builder as _builder 9 | 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( 16 | b'\n\x0f\x66ile_info.proto\x12\x07\x65xample"/\n\x0c\x46ileInfoData\x12\x0c\n\x04\x64ir3\x18\x01 \x01(\t\x12\x11\n\tfile_size\x18\x02 \x01(\rb\x06proto3' 17 | ) 18 | 19 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) 20 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "file_info_pb2", globals()) 21 | if _descriptor._USE_C_DESCRIPTORS == False: 22 | 23 | DESCRIPTOR._options = None 24 | _FILEINFODATA._serialized_start = 28 25 | _FILEINFODATA._serialized_end = 75 26 | # @@protoc_insertion_point(module_scope) 27 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/parser/util/protocbuf/msg.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package app.protobuf; 3 | option go_package=".;proto"; 4 | 5 | message SubMessage1 { 6 | int32 field1 = 1; 7 | int32 field2 = 2; 8 | } 9 | 10 | message SubMessage2 { 11 | int32 field1 = 1; 12 | string field2 = 2; 13 | } 14 | 15 | message MessageBytesExtra { 16 | SubMessage1 message1 = 1; 17 | repeated SubMessage2 message2 = 3; 18 | } 19 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/parser/util/protocbuf/msg_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: msg.proto 4 | """Generated protocol buffer code.""" 5 | from google.protobuf import descriptor as _descriptor 6 | from google.protobuf import descriptor_pool as _descriptor_pool 7 | from google.protobuf import message as _message 8 | from google.protobuf import reflection as _reflection 9 | from google.protobuf import symbol_database as _symbol_database 10 | 11 | # @@protoc_insertion_point(imports) 12 | 13 | _sym_db = _symbol_database.Default() 14 | 15 | 16 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( 17 | b'\n\tmsg.proto\x12\x0c\x61pp.protobuf"-\n\x0bSubMessage1\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\x05\x12\x0e\n\x06\x66ield2\x18\x02 \x01(\x05"-\n\x0bSubMessage2\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\x05\x12\x0e\n\x06\x66ield2\x18\x02 \x01(\t"m\n\x11MessageBytesExtra\x12+\n\x08message1\x18\x01 \x01(\x0b\x32\x19.app.protobuf.SubMessage1\x12+\n\x08message2\x18\x03 \x03(\x0b\x32\x19.app.protobuf.SubMessage2b\x06proto3' 18 | ) 19 | 20 | 21 | _SUBMESSAGE1 = DESCRIPTOR.message_types_by_name["SubMessage1"] 22 | _SUBMESSAGE2 = DESCRIPTOR.message_types_by_name["SubMessage2"] 23 | _MESSAGEBYTESEXTRA = DESCRIPTOR.message_types_by_name["MessageBytesExtra"] 24 | SubMessage1 = _reflection.GeneratedProtocolMessageType( 25 | "SubMessage1", 26 | (_message.Message,), 27 | { 28 | "DESCRIPTOR": _SUBMESSAGE1, 29 | "__module__": "msg_pb2", 30 | # @@protoc_insertion_point(class_scope:app.protobuf.SubMessage1) 31 | }, 32 | ) 33 | _sym_db.RegisterMessage(SubMessage1) 34 | 35 | SubMessage2 = _reflection.GeneratedProtocolMessageType( 36 | "SubMessage2", 37 | (_message.Message,), 38 | { 39 | "DESCRIPTOR": _SUBMESSAGE2, 40 | "__module__": "msg_pb2", 41 | # @@protoc_insertion_point(class_scope:app.protobuf.SubMessage2) 42 | }, 43 | ) 44 | _sym_db.RegisterMessage(SubMessage2) 45 | 46 | MessageBytesExtra = _reflection.GeneratedProtocolMessageType( 47 | "MessageBytesExtra", 48 | (_message.Message,), 49 | { 50 | "DESCRIPTOR": _MESSAGEBYTESEXTRA, 51 | "__module__": "msg_pb2", 52 | # @@protoc_insertion_point(class_scope:app.protobuf.MessageBytesExtra) 53 | }, 54 | ) 55 | _sym_db.RegisterMessage(MessageBytesExtra) 56 | 57 | if _descriptor._USE_C_DESCRIPTORS == False: 58 | 59 | DESCRIPTOR._options = None 60 | _SUBMESSAGE1._serialized_start = 27 61 | _SUBMESSAGE1._serialized_end = 72 62 | _SUBMESSAGE2._serialized_start = 74 63 | _SUBMESSAGE2._serialized_end = 119 64 | _MESSAGEBYTESEXTRA._serialized_start = 121 65 | _MESSAGEBYTESEXTRA._serialized_end = 230 66 | # @@protoc_insertion_point(module_scope) 67 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/parser/util/protocbuf/packed_info_data.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package example; 4 | 5 | // 顶级消息定义 6 | message PackedInfoData { 7 | // varint 类型字段,根据数值范围选用 uint32 或 uint64 8 | uint32 field1 = 1; 9 | uint32 field2 = 2; 10 | MessageField5 info = 5; 11 | } 12 | 13 | // 定义 field14 对应的嵌套消息 14 | // 修改后的嵌套消息,用于 field 14 15 | message MessageField5 { 16 | uint32 field1 = 1; 17 | string audioTxt = 2; // 语音转文字结果 18 | } 19 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/parser/util/protocbuf/packed_info_data_img.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | // 2025年3月微信测试版修改了img命名方式才有了这个东西 3 | message PackedInfoDataImg { 4 | int32 field1 = 1; 5 | int32 field2 = 2; 6 | string filename = 3; 7 | } -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/parser/util/protocbuf/packed_info_data_img2.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | // 2025年3月微信4.0.3正式版修改了img命名方式才有了这个东西 3 | message PackedInfoDataImg2 { 4 | int32 field1 = 1; 5 | int32 field2 = 2; 6 | ImageInfo imageInfo = 3; // 图片 7 | VideoInfo videoInfo = 4; // 视频 8 | AudioInfo audioInfo = 5; // 语音 9 | FileInfo fileInfo = 7; // 文件 10 | MergeInfo mergeInfo = 9; // 合并转发的聊天记录 11 | } 12 | 13 | message ImageInfo { 14 | int32 height = 1; 15 | int32 width = 2; 16 | string filename = 4; 17 | } 18 | 19 | message VideoInfo { 20 | int32 height = 4; 21 | int32 width = 5; 22 | string filename = 8; 23 | } 24 | 25 | message FileInfo { 26 | FileSubMessage1 fileInfo = 1; 27 | FileSubMessage2 field2 = 2; 28 | string field3 = 3; 29 | } 30 | 31 | message FileSubMessage1 { 32 | int32 field1 = 1; 33 | string filename = 2; 34 | } 35 | 36 | message FileSubMessage2 { 37 | string field1 = 1; 38 | string field2 = 2; 39 | string field3 = 3; 40 | } 41 | 42 | message MergeInfo { 43 | string dir = 1; 44 | } 45 | 46 | message AudioInfo { 47 | uint32 field1 = 1; 48 | string audioTxt = 2; // 语音转文字结果 49 | } -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/parser/util/protocbuf/packed_info_data_img2_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: packed_info_data_img2.proto 4 | """Generated protocol buffer code.""" 5 | from google.protobuf import descriptor as _descriptor 6 | from google.protobuf import descriptor_pool as _descriptor_pool 7 | from google.protobuf import symbol_database as _symbol_database 8 | from google.protobuf.internal import builder as _builder 9 | 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( 16 | b'\n\x1bpacked_info_data_img2.proto"\x8f\x01\n\x12PackedInfoDataImg2\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\x05\x12\x0e\n\x06\x66ield2\x18\x02 \x01(\x05\x12\x1d\n\timageInfo\x18\x03 \x01(\x0b\x32\n.ImageInfo\x12\x1d\n\tvideoInfo\x18\x04 \x01(\x0b\x32\n.VideoInfo\x12\x1b\n\x08\x66ileInfo\x18\x07 \x01(\x0b\x32\t.FileInfo"<\n\tImageInfo\x12\x0e\n\x06height\x18\x01 \x01(\x05\x12\r\n\x05width\x18\x02 \x01(\x05\x12\x10\n\x08\x66ilename\x18\x04 \x01(\t"<\n\tVideoInfo\x12\x0e\n\x06height\x18\x04 \x01(\x05\x12\r\n\x05width\x18\x05 \x01(\x05\x12\x10\n\x08\x66ilename\x18\x08 \x01(\t"`\n\x08\x46ileInfo\x12"\n\x08\x66ileInfo\x18\x01 \x01(\x0b\x32\x10.FileSubMessage1\x12 \n\x06\x66ield2\x18\x02 \x01(\x0b\x32\x10.FileSubMessage2\x12\x0e\n\x06\x66ield3\x18\x03 \x01(\t"3\n\x0f\x46ileSubMessage1\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\x05\x12\x10\n\x08\x66ilename\x18\x02 \x01(\t"A\n\x0f\x46ileSubMessage2\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\t\x12\x0e\n\x06\x66ield2\x18\x02 \x01(\t\x12\x0e\n\x06\x66ield3\x18\x03 \x01(\tb\x06proto3' 17 | ) 18 | 19 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) 20 | _builder.BuildTopDescriptorsAndMessages( 21 | DESCRIPTOR, "packed_info_data_img2_pb2", globals() 22 | ) 23 | if _descriptor._USE_C_DESCRIPTORS == False: 24 | 25 | DESCRIPTOR._options = None 26 | _PACKEDINFODATAIMG2._serialized_start = 32 27 | _PACKEDINFODATAIMG2._serialized_end = 175 28 | _IMAGEINFO._serialized_start = 177 29 | _IMAGEINFO._serialized_end = 237 30 | _VIDEOINFO._serialized_start = 239 31 | _VIDEOINFO._serialized_end = 299 32 | _FILEINFO._serialized_start = 301 33 | _FILEINFO._serialized_end = 397 34 | _FILESUBMESSAGE1._serialized_start = 399 35 | _FILESUBMESSAGE1._serialized_end = 450 36 | _FILESUBMESSAGE2._serialized_start = 452 37 | _FILESUBMESSAGE2._serialized_end = 517 38 | # @@protoc_insertion_point(module_scope) 39 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/parser/util/protocbuf/packed_info_data_img_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: packed_info_data_img.proto 4 | """Generated protocol buffer code.""" 5 | from google.protobuf import descriptor as _descriptor 6 | from google.protobuf import descriptor_pool as _descriptor_pool 7 | from google.protobuf import symbol_database as _symbol_database 8 | from google.protobuf.internal import builder as _builder 9 | 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( 16 | b'\n\x1apacked_info_data_img.proto"E\n\x11PackedInfoDataImg\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\x05\x12\x0e\n\x06\x66ield2\x18\x02 \x01(\x05\x12\x10\n\x08\x66ilename\x18\x03 \x01(\tb\x06proto3' 17 | ) 18 | 19 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) 20 | _builder.BuildTopDescriptorsAndMessages( 21 | DESCRIPTOR, "packed_info_data_img_pb2", globals() 22 | ) 23 | if _descriptor._USE_C_DESCRIPTORS == False: 24 | 25 | DESCRIPTOR._options = None 26 | _PACKEDINFODATAIMG._serialized_start = 30 27 | _PACKEDINFODATAIMG._serialized_end = 99 28 | # @@protoc_insertion_point(module_scope) 29 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/parser/util/protocbuf/packed_info_data_merged.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message PackedInfoData { 4 | int32 field1 = 1; 5 | int32 field2 = 2; 6 | FileInfo fileInfo = 7; 7 | AnotherNestedMessage info = 9; 8 | } 9 | 10 | message FileInfo { 11 | SubMessage1 fileInfo = 1; 12 | SubMessage2 field2 = 2; 13 | string field3 = 3; 14 | } 15 | 16 | message SubMessage1 { 17 | int32 field1 = 1; 18 | string filename = 2; 19 | } 20 | 21 | message SubMessage2 { 22 | string field1 = 1; 23 | string field2 = 2; 24 | string field3 = 3; 25 | } 26 | 27 | message AnotherNestedMessage { 28 | string dir = 1; 29 | } 30 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/parser/util/protocbuf/packed_info_data_merged_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: packed_info_data_merged.proto 4 | """Generated protocol buffer code.""" 5 | from google.protobuf import descriptor as _descriptor 6 | from google.protobuf import descriptor_pool as _descriptor_pool 7 | from google.protobuf import symbol_database as _symbol_database 8 | from google.protobuf.internal import builder as _builder 9 | 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( 16 | b'\n\x1dpacked_info_data_merged.proto"u\n\x0ePackedInfoData\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\x05\x12\x0e\n\x06\x66ield2\x18\x02 \x01(\x05\x12\x1e\n\x06\x66ield7\x18\x07 \x01(\x0b\x32\x0e.NestedMessage\x12#\n\x04info\x18\t \x01(\x0b\x32\x15.AnotherNestedMessage"[\n\rNestedMessage\x12\x1c\n\x06\x66ield1\x18\x01 \x01(\x0b\x32\x0c.SubMessage1\x12\x1c\n\x06\x66ield2\x18\x02 \x01(\x0b\x32\x0c.SubMessage2\x12\x0e\n\x06\x66ield3\x18\x03 \x01(\t"-\n\x0bSubMessage1\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\x05\x12\x0e\n\x06\x66ield2\x18\x02 \x01(\t"=\n\x0bSubMessage2\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\t\x12\x0e\n\x06\x66ield2\x18\x02 \x01(\t\x12\x0e\n\x06\x66ield3\x18\x03 \x01(\t"#\n\x14\x41notherNestedMessage\x12\x0b\n\x03\x64ir\x18\x01 \x01(\tb\x06proto3' 17 | ) 18 | 19 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) 20 | _builder.BuildTopDescriptorsAndMessages( 21 | DESCRIPTOR, "packed_info_data_merged_pb2", globals() 22 | ) 23 | if _descriptor._USE_C_DESCRIPTORS == False: 24 | 25 | DESCRIPTOR._options = None 26 | _PACKEDINFODATA._serialized_start = 33 27 | _PACKEDINFODATA._serialized_end = 150 28 | _NESTEDMESSAGE._serialized_start = 152 29 | _NESTEDMESSAGE._serialized_end = 243 30 | _SUBMESSAGE1._serialized_start = 245 31 | _SUBMESSAGE1._serialized_end = 290 32 | _SUBMESSAGE2._serialized_start = 292 33 | _SUBMESSAGE2._serialized_end = 353 34 | _ANOTHERNESTEDMESSAGE._serialized_start = 355 35 | _ANOTHERNESTEDMESSAGE._serialized_end = 390 36 | # @@protoc_insertion_point(module_scope) 37 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/parser/util/protocbuf/packed_info_data_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: packed_info_data.proto 4 | """Generated protocol buffer code.""" 5 | from google.protobuf import descriptor as _descriptor 6 | from google.protobuf import descriptor_pool as _descriptor_pool 7 | from google.protobuf import symbol_database as _symbol_database 8 | from google.protobuf.internal import builder as _builder 9 | 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( 16 | b'\n\x16packed_info_data.proto\x12\x07\x65xample"V\n\x0ePackedInfoData\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\r\x12\x0e\n\x06\x66ield2\x18\x02 \x01(\r\x12$\n\x04info\x18\x05 \x01(\x0b\x32\x16.example.MessageField5"1\n\rMessageField5\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\r\x12\x10\n\x08\x61udioTxt\x18\x02 \x01(\tb\x06proto3' 17 | ) 18 | 19 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) 20 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "packed_info_data_pb2", globals()) 21 | if _descriptor._USE_C_DESCRIPTORS == False: 22 | 23 | DESCRIPTOR._options = None 24 | _PACKEDINFODATA._serialized_start = 35 25 | _PACKEDINFODATA._serialized_end = 121 26 | _MESSAGEFIELD5._serialized_start = 123 27 | _MESSAGEFIELD5._serialized_end = 172 28 | # @@protoc_insertion_point(module_scope) 29 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/parser/util/protocbuf/readme.md: -------------------------------------------------------------------------------- 1 | # 说明 2 | 3 | ## 解析 4 | ```shell 5 | protoc --decode_raw < msg_data.txt 6 | ``` 7 | 8 | ## 根据解析结果,设置.proto文件 9 | ```shell 10 | 1 { 11 | 1: 16 12 | 2: 0 13 | } 14 | 3 { 15 | 1: 1 16 | 2: "wxid_4b1t09d63spw22" 17 | } 18 | 3 { 19 | 1: 7 20 | 2: "\n\t\n\t\t2\n\t\n\t\n\t\tc6680ab2c57499a1a22e44a7eada76e8_\n\t\n\t1\n\t198\n\tv1_Gj7hfmi5\n\t\n\t\t\n\t\n\n" 21 | } 22 | 3 { 23 | 1: 2 24 | 2: "c13acbc95512d1a59bb686d684fd64d8" 25 | } 26 | 3 { 27 | 1: 4 28 | 2: "yiluoAK_47\\FileStorage\\Cache\\2023-08\\2286b5852db82f6cbd9c2084ccd52358" 29 | } 30 | ``` 31 | ## 生成python文件 32 | ```shell 33 | protoc --python_out=. msg.proto 34 | ``` -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/parser/util/protocbuf/roomdata.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package app.protobuf; 3 | option go_package=".;proto"; 4 | 5 | message ChatRoomData { 6 | message ChatRoomMember { 7 | string wxID = 1; 8 | string displayName = 2; 9 | int32 state = 3; 10 | } 11 | repeated ChatRoomMember members = 1; 12 | int32 field_2 = 2; 13 | int32 field_3 = 3; 14 | int32 field_4 = 4; 15 | int32 room_capacity = 5; 16 | int32 field_6 = 6; 17 | int64 field_7 = 7; 18 | int64 field_8 = 8; 19 | } -------------------------------------------------------------------------------- /src/omni_bot_sdk/weixin/parser/util/protocbuf/roomdata_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: roomdata.proto 4 | """Generated protocol buffer code.""" 5 | from google.protobuf import descriptor as _descriptor 6 | from google.protobuf import descriptor_pool as _descriptor_pool 7 | from google.protobuf import message as _message 8 | from google.protobuf import reflection as _reflection 9 | from google.protobuf import symbol_database as _symbol_database 10 | 11 | # @@protoc_insertion_point(imports) 12 | 13 | _sym_db = _symbol_database.Default() 14 | 15 | 16 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( 17 | b'\n\x0eroomdata.proto\x12\x0c\x61pp.protobuf"\x8b\x02\n\x0c\x43hatRoomData\x12:\n\x07members\x18\x01 \x03(\x0b\x32).app.protobuf.ChatRoomData.ChatRoomMember\x12\x0f\n\x07\x66ield_2\x18\x02 \x01(\x05\x12\x0f\n\x07\x66ield_3\x18\x03 \x01(\x05\x12\x0f\n\x07\x66ield_4\x18\x04 \x01(\x05\x12\x15\n\rroom_capacity\x18\x05 \x01(\x05\x12\x0f\n\x07\x66ield_6\x18\x06 \x01(\x05\x12\x0f\n\x07\x66ield_7\x18\x07 \x01(\x03\x12\x0f\n\x07\x66ield_8\x18\x08 \x01(\x03\x1a\x42\n\x0e\x43hatRoomMember\x12\x0c\n\x04wxID\x18\x01 \x01(\t\x12\x13\n\x0b\x64isplayName\x18\x02 \x01(\t\x12\r\n\x05state\x18\x03 \x01(\x05\x62\x06proto3' 18 | ) 19 | 20 | 21 | _CHATROOMDATA = DESCRIPTOR.message_types_by_name["ChatRoomData"] 22 | _CHATROOMDATA_CHATROOMMEMBER = _CHATROOMDATA.nested_types_by_name["ChatRoomMember"] 23 | ChatRoomData = _reflection.GeneratedProtocolMessageType( 24 | "ChatRoomData", 25 | (_message.Message,), 26 | { 27 | "ChatRoomMember": _reflection.GeneratedProtocolMessageType( 28 | "ChatRoomMember", 29 | (_message.Message,), 30 | { 31 | "DESCRIPTOR": _CHATROOMDATA_CHATROOMMEMBER, 32 | "__module__": "roomdata_pb2", 33 | # @@protoc_insertion_point(class_scope:app.protobuf.ChatRoomData.ChatRoomMember) 34 | }, 35 | ), 36 | "DESCRIPTOR": _CHATROOMDATA, 37 | "__module__": "roomdata_pb2", 38 | # @@protoc_insertion_point(class_scope:app.protobuf.ChatRoomData) 39 | }, 40 | ) 41 | _sym_db.RegisterMessage(ChatRoomData) 42 | _sym_db.RegisterMessage(ChatRoomData.ChatRoomMember) 43 | 44 | if _descriptor._USE_C_DESCRIPTORS == False: 45 | 46 | DESCRIPTOR._options = None 47 | _CHATROOMDATA._serialized_start = 33 48 | _CHATROOMDATA._serialized_end = 300 49 | _CHATROOMDATA_CHATROOMMEMBER._serialized_start = 234 50 | _CHATROOMDATA_CHATROOMMEMBER._serialized_end = 300 51 | # @@protoc_insertion_point(module_scope) 52 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/yolo/get_model_path.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | 4 | 5 | def get_model_path(model_name): 6 | """ 7 | 获取 yolo 模型文件的绝对路径,兼容源码和打包环境。 8 | """ 9 | if getattr(sys, "frozen", False): 10 | # 打包后 11 | base_dir = Path(sys.executable).parent 12 | model_path = base_dir / "omni_bot_sdk" / "yolo" / "models" / model_name 13 | else: 14 | # 源码 15 | base_dir = Path(__file__).resolve().parent 16 | model_path = base_dir / "models" / model_name 17 | return str(model_path) 18 | -------------------------------------------------------------------------------- /src/omni_bot_sdk/yolo/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weixin-omni/omni-bot-sdk-oss/7c73235fe5d551a35c396b87a419615847239eec/src/omni_bot_sdk/yolo/models/.gitkeep -------------------------------------------------------------------------------- /src/omni_bot_sdk/yolo/models/msg_rec.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weixin-omni/omni-bot-sdk-oss/7c73235fe5d551a35c396b87a419615847239eec/src/omni_bot_sdk/yolo/models/msg_rec.pt --------------------------------------------------------------------------------